Files
vf_react/src/views/Authentication/SignUp/SignUpIllustration.js
2025-10-14 16:24:36 +08:00

444 lines
17 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src\views\Authentication\SignUp/SignUpIllustration.js
import React, { useState, useEffect, useRef } from "react";
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 = isProduction ? "" : process.env.REACT_APP_API_URL;
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>
);
}