feat: bugfix

This commit is contained in:
zdl
2025-10-31 10:33:53 +08:00
parent f05daa3a78
commit 5d8ad5e442
34 changed files with 314 additions and 3579 deletions

View File

@@ -1,160 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import React, { useState } from "react";
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
Heading,
Input,
Stack,
useColorModeValue,
Text,
Link,
InputGroup,
InputRightElement,
IconButton,
useToast,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
export default function SignInBasic() {
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
email: "",
password: "",
});
const navigate = useNavigate();
const toast = useToast();
const { login, isLoading } = useAuth();
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.email || !formData.password) {
toast({
title: "请填写完整信息",
description: "邮箱和密码都是必填项",
status: "warning",
duration: 3000,
isClosable: true,
});
return;
}
const result = await login(formData.email, formData.password, 'email');
if (result.success) {
// 登录成功,跳转到首页
navigate("/home");
}
};
return (
<Flex minH="100vh" align="center" justify="center" bg={useColorModeValue("gray.50", "gray.900")}>
<Stack spacing={8} mx="auto" maxW="lg" py={12} px={6}>
<Stack align="center">
<Heading style={{minWidth: '140px'}} fontSize="4xl" color="blue.600">
价小前投研
</Heading>
<Text fontSize="lg" color="gray.600">
登录您的账户
</Text>
</Stack>
<Box
rounded="lg"
bg={useColorModeValue("white", "gray.700")}
boxShadow="lg"
p={8}
>
<form onSubmit={handleSubmit}>
<Stack spacing={4}>
<FormControl id="email" isRequired>
<FormLabel>邮箱地址</FormLabel>
<Input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="请输入您的邮箱"
/>
</FormControl>
<FormControl id="password" isRequired>
<FormLabel>密码</FormLabel>
<InputGroup>
<Input
type={showPassword ? "text" : "password"}
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="请输入您的密码"
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
variant="ghost"
size="sm"
/>
</InputRightElement>
</InputGroup>
</FormControl>
<Stack spacing={10}>
<Button
type="submit"
bg="blue.600"
color="white"
_hover={{
bg: "blue.700",
}}
isLoading={isLoading}
loadingText="登录中..."
>
登录
</Button>
</Stack>
<Stack pt={6}>
<Text align="center">
还没有账户?{" "}
<Link color="blue.600" onClick={() => navigate("/auth/signup")}>
立即注册
</Link>
</Text>
</Stack>
</Stack>
</form>
</Box>
</Stack>
</Flex>
);
}

View File

@@ -1,207 +0,0 @@
import React, { useState } from "react";
import {
Box,
Button,
FormControl,
FormLabel,
Input,
VStack,
Heading,
Text,
Link,
useColorMode,
InputGroup,
InputRightElement,
IconButton,
Spinner,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
export default function SignInCentered() {
const { colorMode } = useColorMode();
const navigate = useNavigate();
const { login, isLoading } = useAuth();
// 表单状态
const [formData, setFormData] = useState({
email: "",
password: "",
});
// UI状态
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState({});
// 处理输入变化
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// 清除对应字段的错误
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ""
}));
}
};
// 表单验证
const validateForm = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = "邮箱是必填项";
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = "请输入有效的邮箱地址";
}
if (!formData.password) {
newErrors.password = "密码是必填项";
} else if (formData.password.length < 6) {
newErrors.password = "密码至少需要6个字符";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
const result = await login(formData.email, formData.password);
if (result.success) {
// 登录成功,跳转到首页
navigate("/home");
}
};
return (
<Box
minH="100vh"
display="flex"
alignItems="center"
justifyContent="center"
bg={colorMode === "dark" ? "gray.800" : "gray.50"}
p={4}
>
<Box
w="full"
maxW="md"
p={8}
bg={colorMode === "dark" ? "gray.700" : "white"}
borderRadius="lg"
shadow="xl"
>
<VStack spacing={6}>
<Box textAlign="center">
<Heading size="lg" mb={2}>欢迎回来1</Heading>
<Text color="gray.500">请输入您的凭据登录</Text>
</Box>
<form onSubmit={handleSubmit} style={{ width: "100%" }}>
<VStack spacing={4}>
<FormControl isInvalid={!!errors.email}>
<FormLabel>邮箱地址</FormLabel>
<Input
name="email"
type="email"
placeholder="your@email.com"
value={formData.email}
onChange={handleInputChange}
size="lg"
/>
{errors.email && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.email}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.password}>
<FormLabel>密码</FormLabel>
<InputGroup size="lg">
<Input
name="password"
type={showPassword ? "text" : "password"}
placeholder="********"
value={formData.password}
onChange={handleInputChange}
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
</InputGroup>
{errors.password && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.password}
</Text>
)}
</FormControl>
<Button
type="submit"
colorScheme="blue"
w="full"
size="lg"
isLoading={isLoading}
loadingText="登录中..."
>
{isLoading ? <Spinner size="sm" /> : "登录"}
</Button>
</VStack>
</form>
<VStack spacing={3}>
<Text fontSize="sm" textAlign="center">
还没有账户{" "}
<Link
color="blue.500"
onClick={() => navigate("/auth/signup")}
_hover={{ textDecoration: "underline" }}
>
立即注册
</Link>
</Text>
<Box textAlign="center">
<Link
color="gray.500"
fontSize="sm"
_hover={{ color: "blue.500" }}
>
忘记密码
</Link>
<Text color="gray.500" fontSize="sm" mt={2}>
还没有账户{" "}
<Link
color="blue.500"
fontWeight="medium"
_hover={{ textDecoration: "underline" }}
onClick={() => navigate('/auth/sign-up')}
>
立即注册
</Link>
</Text>
</Box>
</VStack>
</VStack>
</Box>
</Box>
);
}

View File

@@ -1,223 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
HStack,
Icon,
Input,
Link,
Switch,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import CoverImage from "assets/img/CoverImage.png";
import React from "react";
import { FaApple, FaFacebook, FaGoogle } from "react-icons/fa";
import AuthCover from "layouts/AuthCover";
function SignInCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.400", "white");
const bgForm = useColorModeValue("white", "navy.800");
const titleColor = useColorModeValue("gray.700", "blue.500");
const colorIcons = useColorModeValue("gray.700", "white");
const bgIcons = useColorModeValue("trasnparent", "navy.700");
const bgIconsHover = useColorModeValue("gray.50", "whiteAlpha.100");
return (
<AuthCover image={CoverImage}>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "160px" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontSize="xl"
color={textColor}
fontWeight="bold"
textAlign="center"
mb="22px"
>
Sign In with
</Text>
<HStack spacing="15px" justify="center" mb="22px">
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon as={FaFacebook} color={colorIcons} w="30px" h="30px" />
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaApple}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaGoogle}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
</HStack>
<Text
fontSize="lg"
color="gray.400"
fontWeight="bold"
textAlign="center"
mb="22px"
>
or
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Name
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="text"
placeholder="Your full name"
mb="24px"
size="lg"
/>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Password
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="password"
placeholder="Your password"
mb="24px"
size="lg"
/>
<FormControl display="flex" alignItems="center" mb="24px">
<Switch id="remember-login" colorScheme="blue" me="10px" />
<FormLabel htmlFor="remember-login" mb="0" fontWeight="normal">
Remember me
</FormLabel>
</FormControl>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
SIGN IN
</Button>
</FormControl>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
maxW="100%"
mt="0px"
>
<Text color={textColor} fontWeight="medium">
Dont have an account?
<Link
color={titleColor}
as="span"
ms="5px"
href="#"
fontWeight="bold"
>
Sign up
</Link>
</Text>
</Flex>
</Flex>
</Flex>
</AuthCover>
);
}
export default SignInCover;

View File

@@ -1,538 +0,0 @@
// 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 >
);
}

View File

@@ -1,254 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import React, { useState } from "react";
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
Heading,
Input,
Stack,
useColorModeValue,
Text,
Link,
InputGroup,
InputRightElement,
IconButton,
useToast,
Checkbox,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
export default function SignUpBasic() {
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
confirmPassword: "",
agreeToTerms: false,
});
const navigate = useNavigate();
const toast = useToast();
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (formData.password !== formData.confirmPassword) {
toast({
title: "密码不匹配",
description: "请确保两次输入的密码相同",
status: "error",
duration: 3000,
isClosable: true,
});
return;
}
if (!formData.agreeToTerms) {
toast({
title: "请同意条款",
description: "请阅读并同意用户协议和隐私政策",
status: "error",
duration: 3000,
isClosable: true,
});
return;
}
setIsLoading(true);
// 模拟注册过程
setTimeout(() => {
setIsLoading(false);
toast({
title: "注册成功",
description: "欢迎加入价值前沿投资助手",
status: "success",
duration: 3000,
isClosable: true,
});
navigate("/home");
}, 1500);
};
return (
<Flex minH="100vh" align="center" justify="center" bg={useColorModeValue("gray.50", "gray.900")}>
<Stack spacing={8} mx="auto" maxW="lg" py={12} px={6}>
<Stack align="center">
<Heading fontSize="4xl" color="blue.600">
价小前投研
</Heading>
<Text fontSize="lg" color="gray.600">
创建您的账户
</Text>
</Stack>
<Box
rounded="lg"
bg={useColorModeValue("white", "gray.700")}
boxShadow="lg"
p={8}
>
<form onSubmit={handleSubmit}>
<Stack spacing={4}>
<FormControl id="username" isRequired>
<FormLabel>用户名</FormLabel>
<Input
type="text"
name="username"
value={formData.username}
onChange={handleInputChange}
placeholder="请输入您的用户名"
/>
</FormControl>
<FormControl id="email" isRequired>
<FormLabel>邮箱地址</FormLabel>
<Input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="请输入您的邮箱"
/>
</FormControl>
<FormControl id="password" isRequired>
<FormLabel>密码</FormLabel>
<InputGroup>
<Input
type={showPassword ? "text" : "password"}
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="请输入您的密码"
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
variant="ghost"
size="sm"
/>
</InputRightElement>
</InputGroup>
</FormControl>
<FormControl id="confirmPassword" isRequired>
<FormLabel>确认密码</FormLabel>
<InputGroup>
<Input
type={showConfirmPassword ? "text" : "password"}
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="请再次输入您的密码"
/>
<InputRightElement>
<IconButton
aria-label={showConfirmPassword ? "隐藏密码" : "显示密码"}
icon={showConfirmPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
variant="ghost"
size="sm"
/>
</InputRightElement>
</InputGroup>
</FormControl>
<FormControl id="agreeToTerms">
<Checkbox
name="agreeToTerms"
isChecked={formData.agreeToTerms}
onChange={handleInputChange}
colorScheme="blue"
>
<Text fontSize="sm">
我已阅读并同意{" "}
<Link color="blue.600" href="#" isExternal>
用户协议
</Link>{" "}
{" "}
<Link color="blue.600" href="#" isExternal>
隐私政策
</Link>
</Text>
</Checkbox>
</FormControl>
<Stack spacing={10}>
<Button
type="submit"
bg="blue.600"
color="white"
_hover={{
bg: "blue.700",
}}
isLoading={isLoading}
loadingText="注册中..."
>
注册
</Button>
</Stack>
<Stack pt={6}>
<Text align="center">
已有账户?{" "}
<Link color="blue.600" onClick={() => navigate("/auth/signin")}>
立即登录
</Link>
</Text>
</Stack>
</Stack>
</form>
</Box>
</Stack>
</Flex>
);
}

View File

@@ -1,282 +0,0 @@
import React, { useState } from "react";
import {
Box,
Button,
FormControl,
FormLabel,
Input,
VStack,
Heading,
Text,
Link,
useColorMode,
InputGroup,
InputRightElement,
IconButton,
Spinner,
Checkbox,
HStack,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
export default function SignUpCentered() {
const { colorMode } = useColorMode();
const navigate = useNavigate();
const { register, isLoading } = useAuth();
// 表单状态
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
confirmPassword: "",
});
// UI状态
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState({});
const [agreedToTerms, setAgreedToTerms] = useState(false);
// 处理输入变化
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// 清除对应字段的错误
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ""
}));
}
};
// 表单验证
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = "姓名是必填项";
} else if (formData.name.trim().length < 2) {
newErrors.name = "姓名至少需要2个字符";
}
if (!formData.email) {
newErrors.email = "邮箱是必填项";
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = "请输入有效的邮箱地址";
}
if (!formData.password) {
newErrors.password = "密码是必填项";
} else if (formData.password.length < 6) {
newErrors.password = "密码至少需要6个字符";
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)) {
newErrors.password = "密码必须包含大小写字母和数字";
}
if (!formData.confirmPassword) {
newErrors.confirmPassword = "请确认密码";
} else if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "两次输入的密码不一致";
}
if (!agreedToTerms) {
newErrors.terms = "请同意服务条款和隐私政策";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
const result = await register(
formData.name, // username
formData.email,
formData.password
);
if (result.success) {
// 注册成功,跳转到首页
navigate("/home");
}
};
return (
<Box
minH="100vh"
display="flex"
alignItems="center"
justifyContent="center"
bg={colorMode === "dark" ? "gray.800" : "gray.50"}
p={4}
>
<Box
w="full"
maxW="md"
p={8}
bg={colorMode === "dark" ? "gray.700" : "white"}
borderRadius="lg"
shadow="xl"
>
<VStack spacing={6}>
<Box textAlign="center">
<Heading size="lg" mb={2}>创建账户</Heading>
<Text color="gray.500">加入价值前沿开启智能投资之旅</Text>
</Box>
<form onSubmit={handleSubmit} style={{ width: "100%" }}>
<VStack spacing={4}>
<FormControl isInvalid={!!errors.name}>
<FormLabel>姓名</FormLabel>
<Input
name="name"
placeholder="您的姓名"
value={formData.name}
onChange={handleInputChange}
size="lg"
/>
{errors.name && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.name}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.email}>
<FormLabel>邮箱地址</FormLabel>
<Input
name="email"
type="email"
placeholder="your@email.com"
value={formData.email}
onChange={handleInputChange}
size="lg"
/>
{errors.email && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.email}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.password}>
<FormLabel>密码</FormLabel>
<InputGroup size="lg">
<Input
name="password"
type={showPassword ? "text" : "password"}
placeholder="********"
value={formData.password}
onChange={handleInputChange}
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
</InputGroup>
{errors.password && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.password}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.confirmPassword}>
<FormLabel>确认密码</FormLabel>
<InputGroup size="lg">
<Input
name="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder="********"
value={formData.confirmPassword}
onChange={handleInputChange}
/>
<InputRightElement>
<IconButton
aria-label={showConfirmPassword ? "隐藏密码" : "显示密码"}
icon={showConfirmPassword ? <ViewOffIcon /> : <ViewIcon />}
variant="ghost"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
/>
</InputRightElement>
</InputGroup>
{errors.confirmPassword && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.confirmPassword}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.terms}>
<HStack spacing={3}>
<Checkbox
isChecked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
colorScheme="blue"
>
<Text fontSize="sm">
我同意{" "}
<Link color="blue.500" _hover={{ textDecoration: "underline" }}>
服务条款
</Link>
{" "}{" "}
<Link color="blue.500" _hover={{ textDecoration: "underline" }}>
隐私政策
</Link>
</Text>
</Checkbox>
</HStack>
{errors.terms && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.terms}
</Text>
)}
</FormControl>
<Button
type="submit"
colorScheme="blue"
w="full"
size="lg"
isLoading={isLoading}
loadingText="注册中..."
>
{isLoading ? <Spinner size="sm" /> : "创建账户"}
</Button>
</VStack>
</form>
<VStack spacing={3}>
<Text fontSize="sm" textAlign="center">
已有账户{" "}
<Link
color="blue.500"
onClick={() => navigate("/auth/signin")}
_hover={{ textDecoration: "underline" }}
>
立即登录
</Link>
</Text>
</VStack>
</VStack>
</Box>
</Box>
);
}

View File

@@ -1,234 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
HStack,
Icon,
Input,
Link,
Switch,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import CoverImage from "assets/img/CoverImage.png";
import React from "react";
import { FaApple, FaFacebook, FaGoogle } from "react-icons/fa";
import AuthCover from "layouts/AuthCover";
function SignUpCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.400", "white");
const bgForm = useColorModeValue("white", "navy.800");
const titleColor = useColorModeValue("gray.700", "blue.500");
const colorIcons = useColorModeValue("gray.700", "white");
const bgIcons = useColorModeValue("trasnparent", "navy.700");
const bgIconsHover = useColorModeValue("gray.50", "whiteAlpha.100");
return (
<AuthCover image={CoverImage}>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "160px" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontSize="xl"
color={textColor}
fontWeight="bold"
textAlign="center"
mb="22px"
>
Sign In with
</Text>
<HStack spacing="15px" justify="center" mb="22px">
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon as={FaFacebook} color={colorIcons} w="30px" h="30px" />
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaApple}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaGoogle}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
</HStack>
<Text
fontSize="lg"
color="gray.400"
fontWeight="bold"
textAlign="center"
mb="22px"
>
or
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Name
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="text"
placeholder="Your full name"
mb="24px"
size="lg"
/>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Email
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="email"
placeholder="Your full email adress"
mb="24px"
size="lg"
/>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Password
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="password"
placeholder="Your password"
mb="24px"
size="lg"
/>
<FormControl display="flex" alignItems="center" mb="24px">
<Switch id="remember-login" colorScheme="blue" me="10px" />
<FormLabel htmlFor="remember-login" mb="0" fontWeight="normal">
Remember me
</FormLabel>
</FormControl>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
SIGN IN
</Button>
</FormControl>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
maxW="100%"
mt="0px"
>
<Text color={textColor} fontWeight="medium">
Dont have an account?
<Link
color={titleColor}
as="span"
ms="5px"
href="#"
fontWeight="bold"
>
Sign up
</Link>
</Text>
</Flex>
</Flex>
</Flex>
</AuthCover>
);
}
export default SignUpCover;

View File

@@ -1,445 +0,0 @@
// src\views\Authentication\SignUp/SignUpIllustration.js
import React, { useState, useEffect, useRef } from "react";
import { getApiBase } from '../../../utils/apiConfig';
import {
Box,
Button,
Flex,
FormControl,
Input,
Text,
Heading,
VStack,
HStack,
useToast,
InputGroup,
InputRightElement,
IconButton,
Center,
FormErrorMessage,
Link as ChakraLink,
useDisclosure
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
import axios from "axios";
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 PrivacyPolicyModal from '../../../components/PrivacyPolicyModal';
import UserAgreementModal from '../../../components/UserAgreementModal';
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = getApiBase();
export default function SignUpPage() {
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [countdown, setCountdown] = useState(0);
const [errors, setErrors] = useState({});
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "",
password: "",
confirmPassword: "",
verificationCode: ""
});
const navigate = useNavigate();
const toast = useToast();
// 追踪组件挂载状态,防止内存泄漏
const isMountedRef = useRef(true);
// 隐私政策弹窗状态
const { isOpen: isPrivacyModalOpen, onOpen: onPrivacyModalOpen, onClose: onPrivacyModalClose } = useDisclosure();
// 用户协议弹窗状态
const { isOpen: isUserAgreementModalOpen, onOpen: onUserAgreementModalOpen, onClose: onUserAgreementModalClose } = useDisclosure();
// 验证码登录状态 是否开启验证码
const [useVerificationCode, setUseVerificationCode] = useState(false);
// 切换注册方式
const handleChangeMethod = () => {
setUseVerificationCode(!useVerificationCode);
// 切换到密码模式时清空验证码
if (useVerificationCode) {
setFormData(prev => ({ ...prev, verificationCode: "" }));
}
};
// 发送验证码
const sendVerificationCode = async () => {
const contact = formData.phone;
const endpoint = "send-sms-code";
const fieldName = "phone";
if (!contact) {
toast({
title: "请输入手机号",
status: "warning",
duration: 2000,
});
return;
}
if (!/^1[3-9]\d{9}$/.test(contact)) {
toast({
title: "请输入正确的手机号",
status: "warning",
duration: 2000,
});
return;
}
try {
setIsLoading(true);
const response = await axios.post(`${API_BASE_URL}/api/auth/${endpoint}`, {
[fieldName]: contact
}, {
timeout: 10000 // 添加10秒超时
});
// 组件卸载后不再执行后续操作
if (!isMountedRef.current) return;
// ✅ 安全检查:验证 response 和 data 存在
if (!response || !response.data) {
throw new Error('服务器响应为空');
}
toast({
title: "验证码已发送",
description: "请查收短信",
status: "success",
duration: 3000,
});
setCountdown(60);
} catch (error) {
if (isMountedRef.current) {
toast({
title: "发送失败",
description: error.response?.data?.error || error.message || "请稍后重试",
status: "error",
duration: 3000,
});
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
}
};
// 倒计时效果
useEffect(() => {
let isMounted = true;
if (countdown > 0) {
const timer = setTimeout(() => {
if (isMounted) {
setCountdown(countdown - 1);
}
}, 1000);
return () => {
isMounted = false;
clearTimeout(timer);
};
}
}, [countdown]);
// 表单验证
const validateForm = () => {
const newErrors = {};
// 手机号验证(两种方式都需要)
if (!formData.phone) {
newErrors.phone = "请输入手机号";
} else if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
newErrors.phone = "请输入正确的手机号";
}
if (useVerificationCode) {
// 验证码注册方式:只验证手机号和验证码
if (!formData.verificationCode) {
newErrors.verificationCode = "请输入验证码";
}
} else {
// 密码注册方式:验证用户名、密码和确认密码
if (!formData.password || formData.password.length < 6) {
newErrors.password = "密码至少6个字符";
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "两次密码不一致";
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理注册提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
let endpoint, data;
if (useVerificationCode) {
// 验证码注册:只发送手机号和验证码
endpoint = "/api/auth/register/phone-code";
data = {
phone: formData.phone,
code: formData.verificationCode
};
} else {
// 密码注册:发送手机号、用户名和密码
endpoint = "/api/auth/register/phone";
data = {
phone: formData.phone,
username: formData.username,
password: formData.password
};
}
const response = await axios.post(`${API_BASE_URL}${endpoint}`, data, {
timeout: 10000 // 添加10秒超时
});
// 组件卸载后不再执行后续操作
if (!isMountedRef.current) return;
// ✅ 安全检查:验证 response 和 data 存在
if (!response || !response.data) {
throw new Error('注册请求失败:服务器响应为空');
}
toast({
title: "注册成功",
description: "即将跳转到登录页面",
status: "success",
duration: 2000,
});
setTimeout(() => {
if (isMountedRef.current) {
navigate("/auth/sign-in");
}
}, 2000);
} catch (error) {
if (isMountedRef.current) {
toast({
title: "注册失败",
description: error.response?.data?.error || error.message || "请稍后重试",
status: "error",
duration: 3000,
});
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: "" }));
}
};
// 组件卸载时清理
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
// 公用的用户名和密码输入框组件
const commonAuthFields = (
<VStack spacing={4} width="100%">
<FormControl isRequired isInvalid={!!errors.password}>
<InputGroup>
<Input
name="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={handleInputChange}
placeholder="设置密码至少6个字符"
pr="3rem"
/>
<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>
<FormControl isRequired isInvalid={!!errors.confirmPassword}>
<Input
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="再次输入密码"
/>
<FormErrorMessage>{errors.confirmPassword}</FormErrorMessage>
</FormControl>
</VStack>
);
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={handleSubmit}>
<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>
{/* 表单字段区域 */}
<Box width="100%">
{
useVerificationCode ? (
<VStack spacing={4} width="100%">
<VerificationCodeInput
value={formData.verificationCode}
onChange={handleInputChange}
onSendCode={sendVerificationCode}
countdown={countdown}
isLoading={isLoading}
isSending={isLoading && countdown === 0}
error={errors.verificationCode}
colorScheme="green"
/>
{/* 隐藏的占位元素,保持与密码模式相同的高度 */}
<Box height="40px" width="100%" visibility="hidden" />
</VStack>
) : (
<>
{commonAuthFields}
</>
)
}
</Box>
<AuthFooter
linkText="已有账号?"
linkLabel="去登录"
linkTo="/auth/sign-in"
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"
>
注册
</Button>
{/* 协议同意文本 */}
<Text fontSize="sm" color="gray.600" textAlign="center" width="100%">
注册登录即表示阅读并同意{" "}
<ChakraLink
color="blue.500"
fontSize="sm"
onClick={onUserAgreementModalOpen}
textDecoration="underline"
_hover={{ color: "blue.600" }}
cursor="pointer"
>
用户协议
</ChakraLink>
{" "}{" "}
<ChakraLink
color="blue.500"
fontSize="sm"
onClick={onPrivacyModalOpen}
textDecoration="underline"
_hover={{ color: "blue.600" }}
cursor="pointer"
>
隐私政策
</ChakraLink>
</Text>
</VStack>
</form>
</Box>
{/* 右侧:微信注册 - 20% 宽度 */}
<Box flex="1">
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
<WechatRegister />
</Center>
</Box>
</HStack>
</Box>
</Flex>
{/* 隐私政策弹窗 */}
<PrivacyPolicyModal isOpen={isPrivacyModalOpen} onClose={onPrivacyModalClose} />
{/* 用户协议弹窗 */}
<UserAgreementModal isOpen={isUserAgreementModalOpen} onClose={onUserAgreementModalClose} />
</Flex>
);
}