feat: bugfix
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
Don’t have an account?
|
||||
<Link
|
||||
color={titleColor}
|
||||
as="span"
|
||||
ms="5px"
|
||||
href="#"
|
||||
fontWeight="bold"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AuthCover>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignInCover;
|
||||
@@ -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 >
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
Don’t have an account?
|
||||
<Link
|
||||
color={titleColor}
|
||||
as="span"
|
||||
ms="5px"
|
||||
href="#"
|
||||
fontWeight="bold"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AuthCover>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpCover;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user