feat: 登陆注册UI调整,用户协议和隐私政策跳转调整
This commit is contained in:
379
src/components/Auth/AuthFormContent.js
Normal file
379
src/components/Auth/AuthFormContent.js
Normal file
@@ -0,0 +1,379 @@
|
||||
// src/components/Auth/AuthFormContent.js
|
||||
// 统一的认证表单组件
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Heading,
|
||||
VStack,
|
||||
HStack,
|
||||
useToast,
|
||||
Icon,
|
||||
FormErrorMessage,
|
||||
Center,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
Text,
|
||||
Link as ChakraLink,
|
||||
} from "@chakra-ui/react";
|
||||
import { FaLock } from "react-icons/fa";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import AuthHeader from './AuthHeader';
|
||||
import VerificationCodeInput from './VerificationCodeInput';
|
||||
import WechatRegister from './WechatRegister';
|
||||
|
||||
// API配置
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = isProduction ? "" : "http://49.232.185.254:5000";
|
||||
|
||||
// 统一配置对象
|
||||
const AUTH_CONFIG = {
|
||||
// UI文本
|
||||
title: "欢迎使用价值前沿",
|
||||
subtitle: "开启您的投资之旅",
|
||||
formTitle: "手机号验证",
|
||||
buttonText: "登录/注册",
|
||||
loadingText: "验证中...",
|
||||
successTitle: "验证成功",
|
||||
successDescription: "欢迎!",
|
||||
errorTitle: "验证失败",
|
||||
|
||||
// API配置
|
||||
api: {
|
||||
endpoint: '/api/auth/register-with-code',
|
||||
purpose: 'register',
|
||||
},
|
||||
|
||||
// 功能开关
|
||||
features: {
|
||||
successDelay: 1000, // 延迟1秒显示成功提示
|
||||
}
|
||||
};
|
||||
|
||||
export default function AuthFormContent() {
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { checkSession } = useAuth();
|
||||
const { handleLoginSuccess } = useAuthModal();
|
||||
|
||||
// 使用统一配置
|
||||
const config = AUTH_CONFIG;
|
||||
|
||||
// 追踪组件挂载状态,防止内存泄漏
|
||||
const isMountedRef = useRef(true);
|
||||
const cancelRef = useRef(); // AlertDialog 需要的 ref
|
||||
|
||||
// 页面状态
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
// 昵称设置引导对话框
|
||||
const [showNicknamePrompt, setShowNicknamePrompt] = useState(false);
|
||||
const [currentPhone, setCurrentPhone] = useState("");
|
||||
|
||||
|
||||
// 表单数据
|
||||
const [formData, setFormData] = useState({
|
||||
phone: "",
|
||||
verificationCode: "",
|
||||
});
|
||||
|
||||
// 验证码状态
|
||||
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
|
||||
const [sendingCode, setSendingCode] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
|
||||
// 输入框变化处理
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
// 倒计时逻辑
|
||||
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;
|
||||
|
||||
if (!credential) {
|
||||
toast({
|
||||
title: "请先输入手机号",
|
||||
status: "warning",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(credential)) {
|
||||
toast({
|
||||
title: "请输入有效的手机号",
|
||||
status: "warning",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSendingCode(true);
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/send-verification-code`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
credential,
|
||||
type: 'phone',
|
||||
purpose: config.api.purpose // 根据模式使用不同的purpose
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error('网络请求失败,请检查网络连接');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (!data) {
|
||||
throw new Error('服务器响应为空');
|
||||
}
|
||||
|
||||
if (response.ok && data.success) {
|
||||
toast({
|
||||
title: "验证码已发送",
|
||||
description: "验证码已发送到您的手机号",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
});
|
||||
setVerificationCodeSent(true);
|
||||
setCountdown(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 handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { phone, verificationCode, nickname } = formData;
|
||||
|
||||
// 表单验证
|
||||
if (!phone || !verificationCode) {
|
||||
toast({
|
||||
title: "请填写完整信息",
|
||||
description: "手机号和验证码不能为空",
|
||||
status: "warning",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
toast({
|
||||
title: "请输入有效的手机号",
|
||||
status: "warning",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建请求体
|
||||
const requestBody = {
|
||||
credential: phone,
|
||||
verification_code: verificationCode,
|
||||
register_type: 'phone',
|
||||
};
|
||||
|
||||
// 调用API(根据模式选择不同的endpoint)
|
||||
const response = await fetch(`${API_BASE_URL}${config.api.endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error('网络请求失败,请检查网络连接');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (!data) {
|
||||
throw new Error('服务器响应为空');
|
||||
}
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// 更新session
|
||||
await checkSession();
|
||||
|
||||
toast({
|
||||
title: config.successTitle,
|
||||
description: config.successDescription,
|
||||
status: "success",
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// 检查是否为新注册用户
|
||||
if (data.isNewUser) {
|
||||
// 新注册用户,延迟后显示昵称设置引导
|
||||
setTimeout(() => {
|
||||
setCurrentPhone(phone);
|
||||
setShowNicknamePrompt(true);
|
||||
}, config.features.successDelay);
|
||||
} else {
|
||||
// 已有用户,直接登录成功
|
||||
setTimeout(() => {
|
||||
handleLoginSuccess({ phone });
|
||||
}, config.features.successDelay);
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.error || `${config.errorTitle}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth error:', error);
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: config.errorTitle,
|
||||
description: error.message || "请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box width="100%">
|
||||
<AuthHeader title={config.title} subtitle={config.subtitle} />
|
||||
<HStack spacing={8} align="stretch">
|
||||
<Box flex="4">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<VStack spacing={4}>
|
||||
<Heading size="md" color="gray.700" alignSelf="flex-start">{config.formTitle}</Heading>
|
||||
<FormControl isRequired isInvalid={!!errors.phone}>
|
||||
<Input name="phone" value={formData.phone} onChange={handleInputChange} placeholder="请输入11位手机号" />
|
||||
<FormErrorMessage>{errors.phone}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<VerificationCodeInput value={formData.verificationCode} onChange={handleInputChange} onSendCode={sendVerificationCode} countdown={countdown} isLoading={isLoading} isSending={sendingCode} error={errors.verificationCode} colorScheme="green" />
|
||||
<Button type="submit" width="100%" size="lg" colorScheme="green" color="white" borderRadius="lg" isLoading={isLoading} loadingText={config.loadingText} fontWeight="bold"><Icon as={FaLock} mr={2} />{config.buttonText}</Button>
|
||||
|
||||
{/* 隐私声明 */}
|
||||
<Text fontSize="xs" color="gray.500" textAlign="center" mt={2}>
|
||||
登录即表示你同意价值前沿{" "}
|
||||
<ChakraLink
|
||||
as="a"
|
||||
href="/home/user-agreement"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="blue.500"
|
||||
textDecoration="underline"
|
||||
_hover={{ color: "blue.600" }}
|
||||
>
|
||||
《用户协议》
|
||||
</ChakraLink>
|
||||
{" "}和{" "}
|
||||
<ChakraLink
|
||||
as="a"
|
||||
href="/home/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="blue.500"
|
||||
textDecoration="underline"
|
||||
_hover={{ color: "blue.600" }}
|
||||
>
|
||||
《隐私政策》
|
||||
</ChakraLink>
|
||||
</Text>
|
||||
</VStack>
|
||||
</form>
|
||||
</Box>
|
||||
<Box flex="1">
|
||||
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}><WechatRegister /></Center>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* 只在需要时才渲染 AlertDialog,避免创建不必要的 Portal */}
|
||||
{showNicknamePrompt && (
|
||||
<AlertDialog isOpen={showNicknamePrompt} leastDestructiveRef={cancelRef} onClose={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }} isCentered closeOnEsc={true} closeOnOverlayClick={false}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">完善个人信息</AlertDialogHeader>
|
||||
<AlertDialogBody>您已成功注册!是否前往个人中心设置昵称和其他信息?</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }}>稍后再说</Button>
|
||||
<Button colorScheme="green" onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); setTimeout(() => { navigate('/admin/profile'); }, 300); }} ml={3}>去设置</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user