Files
vf_react/src/components/Auth/AuthFormContent.js
2025-10-20 13:28:37 +08:00

495 lines
19 KiB
JavaScript
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/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,
Stack,
useToast,
Icon,
FormErrorMessage,
Center,
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
Text,
Link as ChakraLink,
useBreakpointValue,
Divider,
IconButton,
} from "@chakra-ui/react";
import { FaLock, FaWeixin } from "react-icons/fa";
import { useAuth } from "../../contexts/AuthContext";
import { useAuthModal } from "../../contexts/AuthModalContext";
import { authService } from "../../services/authService";
import AuthHeader from './AuthHeader';
import VerificationCodeInput from './VerificationCodeInput';
import WechatRegister from './WechatRegister';
import { setCurrentUser } from '../../mocks/data/users';
import { logger } from '../../utils/logger';
// 统一配置对象
const AUTH_CONFIG = {
// UI文本
title: "价值前沿",
subtitle: "开启您的投资之旅",
formTitle: "登陆/注册",
buttonText: "登录/注册",
loadingText: "验证中...",
successTitle: "验证成功",
successDescription: "欢迎!",
errorTitle: "验证失败",
// API配置
api: {
endpoint: '/api/auth/register/phone',
purpose: 'login', // ⚡ 统一使用 'login' 模式
},
// 功能开关
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 isMobile = useBreakpointValue({ base: true, md: false });
const stackDirection = useBreakpointValue({ base: "column", md: "row" });
const stackSpacing = useBreakpointValue({ base: 4, md: 8 });
// 表单数据
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 requestData = {
credential: credential.trim(), // 添加 trim() 防止空格
type: 'phone',
purpose: config.api.purpose
};
logger.api.request('POST', '/api/auth/send-verification-code', requestData);
const response = await fetch('/api/auth/send-verification-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(requestData),
});
if (!response) {
throw new Error('网络请求失败,请检查网络连接');
}
const data = await response.json();
logger.api.response('POST', '/api/auth/send-verification-code', response.status, data);
if (!isMountedRef.current) return;
if (!data) {
throw new Error('服务器响应为空');
}
if (response.ok && data.success) {
// ❌ 移除成功 toast静默处理
logger.info('AuthFormContent', '验证码发送成功', {
credential: credential.substring(0, 3) + '****' + credential.substring(7),
dev_code: data.dev_code
});
// ✅ 开发环境下在控制台显示验证码
if (data.dev_code) {
console.log(`%c✅ [验证码] ${credential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
}
setVerificationCodeSent(true);
setCountdown(60);
} else {
throw new Error(data.error || '发送验证码失败');
}
} catch (error) {
// ❌ 移除错误 toast仅 console 输出
logger.api.error('POST', '/api/auth/send-verification-code', error, {
credential: credential.substring(0, 3) + '****' + credential.substring(7)
});
} 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.trim(), // 添加 trim() 防止空格
verification_code: verificationCode.trim(), // 添加 trim() 防止空格
login_type: 'phone',
};
logger.api.request('POST', '/api/auth/login-with-code', {
credential: phone.substring(0, 3) + '****' + phone.substring(7),
verification_code: verificationCode.substring(0, 2) + '****',
login_type: 'phone'
});
// 调用API根据模式选择不同的endpoint
const response = await fetch('/api/auth/login-with-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(requestBody),
});
if (!response) {
throw new Error('网络请求失败,请检查网络连接');
}
const data = await response.json();
logger.api.response('POST', '/api/auth/login-with-code', response.status, {
...data,
user: data.user ? { id: data.user.id, phone: data.user.phone } : null
});
if (!isMountedRef.current) return;
if (!data) {
throw new Error('服务器响应为空');
}
if (response.ok && data.success) {
// ⚡ Mock 模式:先在前端侧写入 localStorage确保时序正确
if (process.env.REACT_APP_ENABLE_MOCK === 'true' && data.user) {
setCurrentUser(data.user);
logger.debug('AuthFormContent', '前端侧设置当前用户(Mock模式)', {
userId: data.user?.id,
phone: data.user?.phone,
mockMode: true
});
}
// 更新session
await checkSession();
// ✅ 保留登录成功 toast关键操作提示
toast({
title: data.isNewUser ? '注册成功' : '登录成功',
description: config.successDescription,
status: "success",
duration: 2000,
});
logger.info('AuthFormContent', '登录成功', {
isNewUser: data.isNewUser,
userId: data.user?.id
});
// 检查是否为新注册用户
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) {
// ❌ 移除错误 toast仅 console 输出
const { phone, verificationCode } = formData;
logger.error('AuthFormContent', 'handleSubmit', error, {
phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A',
hasVerificationCode: !!verificationCode
});
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
}
};
// 微信H5登录处理
const handleWechatH5Login = async () => {
try {
// 1. 构建回调URL
const redirectUrl = `${window.location.origin}/home/wechat-callback`;
// 2. 显示提示
toast({
title: "即将跳转",
description: "正在跳转到微信授权页面...",
status: "info",
duration: 2000,
isClosable: true,
});
// 3. 获取微信H5授权URL
const response = await authService.getWechatH5AuthUrl(redirectUrl);
if (!response || !response.auth_url) {
throw new Error('获取授权链接失败');
}
// 4. 延迟跳转,让用户看到提示
setTimeout(() => {
window.location.href = response.auth_url;
}, 500);
} catch (error) {
logger.error('AuthFormContent', 'handleWechatH5Login', error);
toast({
title: "跳转失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
isClosable: true,
});
}
};
// 组件卸载时清理
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return (
<>
<Box width="100%">
<AuthHeader title={config.title} subtitle={config.subtitle} />
<Stack direction={stackDirection} spacing={stackSpacing} align="stretch">
<Box flex={{ base: "1", md: "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>
{/* 验证码输入框 + 移动端微信图标 */}
<Box width="100%" position="relative">
<VerificationCodeInput value={formData.verificationCode} onChange={handleInputChange} onSendCode={sendVerificationCode} countdown={countdown} isLoading={isLoading} isSending={sendingCode} error={errors.verificationCode} colorScheme="green" />
{/* 移动端:验证码下方的微信登录图标 */}
{isMobile && (
<HStack spacing={0} mt={2} alignItems="center">
<Text fontSize="xs" color="gray.500">其他登录方式</Text>
<IconButton
aria-label="微信登录"
icon={<Icon as={FaWeixin} w={4} h={4} />}
size="sm"
variant="ghost"
color="#07C160"
borderRadius="md"
minW="24px"
minH="24px"
_hover={{
bg: "green.50",
color: "#06AD56"
}}
_active={{
bg: "green.100"
}}
onClick={handleWechatH5Login}
isDisabled={isLoading}
/>
</HStack>
)}
</Box>
<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>
{/* 桌面端:右侧二维码扫描 */}
{!isMobile && (
<Box flex="1">
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
<WechatRegister />
</Center>
</Box>
)}
</Stack>
</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('/home/profile'); }, 300); }} ml={3}>去设置</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
)}
</>
);
}