feat: 调整注册逻辑
This commit is contained in:
55
src/components/Auth/AuthBackground.js
Normal file
55
src/components/Auth/AuthBackground.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// src/components/Auth/AuthBackground.js
|
||||
import React from "react";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
|
||||
/**
|
||||
* 认证页面通用背景组件
|
||||
* 用于登录和注册页面的动态渐变背景
|
||||
*/
|
||||
export default function AuthBackground() {
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
zIndex={0}
|
||||
background={`linear-gradient(45deg, rgba(139, 69, 19, 0.9) 0%, rgba(160, 82, 45, 0.8) 15%, rgba(205, 133, 63, 0.7) 30%, rgba(222, 184, 135, 0.8) 45%, rgba(245, 222, 179, 0.6) 60%, rgba(255, 228, 196, 0.7) 75%, rgba(139, 69, 19, 0.8) 100%)`}
|
||||
_before={{
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: `conic-gradient(from 0deg at 30% 20%, rgba(255, 140, 0, 0.6) 0deg, rgba(255, 69, 0, 0.4) 60deg, rgba(139, 69, 19, 0.5) 120deg, rgba(160, 82, 45, 0.6) 180deg, rgba(205, 133, 63, 0.4) 240deg, rgba(255, 140, 0, 0.5) 300deg, rgba(255, 140, 0, 0.6) 360deg)`,
|
||||
mixBlendMode: 'multiply',
|
||||
animation: 'fluid-rotate 20s linear infinite'
|
||||
}}
|
||||
_after={{
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '10%',
|
||||
left: '20%',
|
||||
width: '60%',
|
||||
height: '80%',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(ellipse at center, rgba(255, 165, 0, 0.3) 0%, rgba(255, 140, 0, 0.2) 50%, transparent 70%)',
|
||||
filter: 'blur(40px)',
|
||||
animation: 'wave-pulse 8s ease-in-out infinite'
|
||||
}}
|
||||
sx={{
|
||||
'@keyframes fluid-rotate': {
|
||||
'0%': { transform: 'rotate(0deg) scale(1)' },
|
||||
'50%': { transform: 'rotate(180deg) scale(1.1)' },
|
||||
'100%': { transform: 'rotate(360deg) scale(1)' }
|
||||
},
|
||||
'@keyframes wave-pulse': {
|
||||
'0%, 100%': { opacity: 0.4, transform: 'scale(1)' },
|
||||
'50%': { opacity: 0.8, transform: 'scale(1.2)' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
42
src/components/Auth/AuthFooter.js
Normal file
42
src/components/Auth/AuthFooter.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { HStack, Text, Link as ChakraLink } from "@chakra-ui/react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
/**
|
||||
* 认证页面底部组件
|
||||
* 包含页面切换链接和登录方式切换链接
|
||||
*/
|
||||
export default function AuthFooter({
|
||||
// 左侧链接配置
|
||||
linkText, // 提示文本,如 "还没有账号," 或 "已有账号?"
|
||||
linkLabel, // 链接文本,如 "去注册" 或 "去登录"
|
||||
linkTo, // 链接路径,如 "/auth/sign-up" 或 "/auth/sign-in"
|
||||
|
||||
// 右侧切换配置
|
||||
useVerificationCode, // 当前是否使用验证码登录
|
||||
onSwitchMethod // 切换登录方式的回调函数
|
||||
}) {
|
||||
return (
|
||||
<HStack justify="space-between" width="100%">
|
||||
{/* 左侧:页面切换链接(去注册/去登录) */}
|
||||
<HStack spacing={1} as={Link} to={linkTo}>
|
||||
<Text fontSize="sm" color="gray.600">{linkText}</Text>
|
||||
<Text fontSize="sm" color="blue.500" fontWeight="bold">{linkLabel}</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 右侧:登录方式切换链接 */}
|
||||
<ChakraLink
|
||||
href="#"
|
||||
fontSize="sm"
|
||||
color="blue.500"
|
||||
fontWeight="bold"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onSwitchMethod();
|
||||
}}
|
||||
>
|
||||
{useVerificationCode ? '密码登陆' : '验证码登陆'}
|
||||
</ChakraLink>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
23
src/components/Auth/AuthHeader.js
Normal file
23
src/components/Auth/AuthHeader.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// src/components/Auth/AuthHeader.js
|
||||
import React from "react";
|
||||
import { Heading, Text, VStack } from "@chakra-ui/react";
|
||||
|
||||
/**
|
||||
* 认证页面通用头部组件
|
||||
* 用于显示页面标题和描述
|
||||
*
|
||||
* @param {string} title - 主标题文字
|
||||
* @param {string} subtitle - 副标题文字
|
||||
*/
|
||||
export default function AuthHeader({ title, subtitle }) {
|
||||
return (
|
||||
<VStack spacing={2} mb={8}>
|
||||
<Heading size="xl" color="gray.800" fontWeight="bold">
|
||||
{title}
|
||||
</Heading>
|
||||
<Text color="gray.600" fontSize="md">
|
||||
{subtitle}
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
44
src/components/Auth/VerificationCodeInput.js
Normal file
44
src/components/Auth/VerificationCodeInput.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { FormControl, FormErrorMessage, HStack, Input, Button } from "@chakra-ui/react";
|
||||
|
||||
/**
|
||||
* 通用验证码输入组件
|
||||
*/
|
||||
export default function VerificationCodeInput({
|
||||
value,
|
||||
onChange,
|
||||
onSendCode,
|
||||
countdown,
|
||||
isLoading,
|
||||
isSending,
|
||||
error,
|
||||
placeholder = "请输入6位验证码",
|
||||
buttonText = "获取验证码",
|
||||
countdownText = (count) => `${count}s`,
|
||||
colorScheme = "green",
|
||||
isRequired = true
|
||||
}) {
|
||||
return (
|
||||
<FormControl isRequired={isRequired} isInvalid={!!error}>
|
||||
<HStack>
|
||||
<Input
|
||||
name="verificationCode"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
maxLength={6}
|
||||
/>
|
||||
<Button
|
||||
colorScheme={colorScheme}
|
||||
onClick={onSendCode}
|
||||
isDisabled={countdown > 0 || isLoading}
|
||||
isLoading={isSending}
|
||||
minW="120px"
|
||||
>
|
||||
{countdown > 0 ? countdownText(countdown) : buttonText}
|
||||
</Button>
|
||||
</HStack>
|
||||
<FormErrorMessage>{error}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
330
src/components/Auth/WechatRegister.js
Normal file
330
src/components/Auth/WechatRegister.js
Normal file
@@ -0,0 +1,330 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
VStack,
|
||||
Text,
|
||||
Icon,
|
||||
useToast,
|
||||
Spinner
|
||||
} from "@chakra-ui/react";
|
||||
import { FaQrcode } from "react-icons/fa";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
|
||||
|
||||
// 配置常量
|
||||
const POLL_INTERVAL = 2000; // 轮询间隔:2秒
|
||||
const QR_CODE_TIMEOUT = 300000; // 二维码超时:5分钟
|
||||
|
||||
export default function WechatRegister() {
|
||||
// 状态管理
|
||||
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
|
||||
const [wechatSessionId, setWechatSessionId] = useState("");
|
||||
const [wechatStatus, setWechatStatus] = useState(WECHAT_STATUS.NONE);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 使用 useRef 管理定时器,避免闭包问题和内存泄漏
|
||||
const pollIntervalRef = useRef(null);
|
||||
const timeoutRef = useRef(null);
|
||||
const isMountedRef = useRef(true); // 追踪组件挂载状态
|
||||
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
|
||||
/**
|
||||
* 显示统一的错误提示
|
||||
*/
|
||||
const showError = useCallback((title, description) => {
|
||||
toast({
|
||||
title,
|
||||
description,
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
}, [toast]);
|
||||
|
||||
/**
|
||||
* 显示成功提示
|
||||
*/
|
||||
const showSuccess = useCallback((title, description) => {
|
||||
toast({
|
||||
title,
|
||||
description,
|
||||
status: "success",
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
}, [toast]);
|
||||
|
||||
/**
|
||||
* 清理所有定时器
|
||||
*/
|
||||
const clearTimers = useCallback(() => {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理登录成功
|
||||
*/
|
||||
const handleLoginSuccess = useCallback(async (sessionId, status) => {
|
||||
try {
|
||||
const response = await authService.loginWithWechat(sessionId);
|
||||
|
||||
if (response?.success) {
|
||||
// Session cookie 会自动管理,不需要手动存储
|
||||
// 如果后端返回了 token,可以选择性存储(兼容旧方式)
|
||||
if (response.token) {
|
||||
localStorage.setItem('token', response.token);
|
||||
}
|
||||
if (response.user) {
|
||||
localStorage.setItem('user', JSON.stringify(response.user));
|
||||
}
|
||||
|
||||
showSuccess(
|
||||
status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "注册成功",
|
||||
"正在跳转..."
|
||||
);
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
navigate("/home");
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(response?.error || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
showError("登录失败", error.message || "请重试");
|
||||
}
|
||||
}, [navigate, showSuccess, showError]);
|
||||
|
||||
/**
|
||||
* 检查微信扫码状态
|
||||
*/
|
||||
const checkWechatStatus = useCallback(async () => {
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current || !wechatSessionId) return;
|
||||
|
||||
try {
|
||||
const response = await authService.checkWechatStatus(wechatSessionId);
|
||||
|
||||
// 安全检查:确保 response 存在且包含 status
|
||||
if (!response || typeof response.status === 'undefined') {
|
||||
console.warn('微信状态检查返回无效数据:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = response;
|
||||
|
||||
// 组件卸载后不再更新状态
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setWechatStatus(status);
|
||||
|
||||
// 处理成功状态
|
||||
if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) {
|
||||
clearTimers(); // 停止轮询
|
||||
await handleLoginSuccess(wechatSessionId, status);
|
||||
}
|
||||
// 处理过期状态
|
||||
else if (status === WECHAT_STATUS.EXPIRED) {
|
||||
clearTimers();
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "授权已过期",
|
||||
description: "请重新获取授权",
|
||||
status: "warning",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("检查微信状态失败:", error);
|
||||
// 轮询过程中的错误不显示给用户,避免频繁提示
|
||||
// 但如果错误持续发生,停止轮询避免无限重试
|
||||
if (error.message.includes('网络连接失败')) {
|
||||
clearTimers();
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "网络连接失败",
|
||||
description: "请检查网络后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [wechatSessionId, handleLoginSuccess, clearTimers, toast]);
|
||||
|
||||
/**
|
||||
* 启动轮询
|
||||
*/
|
||||
const startPolling = useCallback(() => {
|
||||
// 清理旧的定时器
|
||||
clearTimers();
|
||||
|
||||
// 启动轮询
|
||||
pollIntervalRef.current = setInterval(() => {
|
||||
checkWechatStatus();
|
||||
}, POLL_INTERVAL);
|
||||
|
||||
// 设置超时
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
clearTimers();
|
||||
setWechatStatus(WECHAT_STATUS.EXPIRED);
|
||||
}, QR_CODE_TIMEOUT);
|
||||
}, [checkWechatStatus, clearTimers]);
|
||||
|
||||
/**
|
||||
* 获取微信二维码
|
||||
*/
|
||||
const getWechatQRCode = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await authService.getWechatQRCode();
|
||||
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 安全检查:确保响应包含必要字段
|
||||
if (!response) {
|
||||
throw new Error('服务器无响应');
|
||||
}
|
||||
|
||||
if (!response.auth_url || !response.session_id) {
|
||||
throw new Error('获取二维码失败:响应数据不完整');
|
||||
}
|
||||
|
||||
setWechatAuthUrl(response.auth_url);
|
||||
setWechatSessionId(response.session_id);
|
||||
setWechatStatus(WECHAT_STATUS.WAITING);
|
||||
|
||||
// 启动轮询检查扫码状态
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
console.error('获取微信授权失败:', error);
|
||||
if (isMountedRef.current) {
|
||||
showError("获取微信授权失败", error.message || "请稍后重试");
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件卸载时清理定时器和标记组件状态
|
||||
*/
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
clearTimers();
|
||||
};
|
||||
}, [clearTimers]);
|
||||
|
||||
/**
|
||||
* 渲染状态提示文本
|
||||
*/
|
||||
const renderStatusText = () => {
|
||||
if (!wechatAuthUrl || wechatStatus === WECHAT_STATUS.NONE || wechatStatus === WECHAT_STATUS.EXPIRED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{STATUS_MESSAGES[wechatStatus] || STATUS_MESSAGES[WECHAT_STATUS.WAITING]}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack spacing={2}>
|
||||
<Text fontSize="lg" fontWeight="bold" color="gray.700" whiteSpace="nowrap">
|
||||
微信扫一扫
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
position="relative"
|
||||
minH="120px"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{/* 灰色二维码底图 - 始终显示 */}
|
||||
<Icon as={FaQrcode} w={24} h={24} color="gray.300" />
|
||||
|
||||
{/* 加载动画 */}
|
||||
{isLoading && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="0"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Spinner
|
||||
size="lg"
|
||||
color="green.500"
|
||||
thickness="4px"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 显示获取/刷新二维码按钮 */}
|
||||
{(wechatStatus === WECHAT_STATUS.NONE || wechatStatus === WECHAT_STATUS.EXPIRED) && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="0"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="rgba(255, 255, 255, 0.3)"
|
||||
backdropFilter="blur(2px)"
|
||||
>
|
||||
<VStack spacing={2}>
|
||||
<Button
|
||||
variant="outline"
|
||||
colorScheme="green"
|
||||
size="sm"
|
||||
onClick={getWechatQRCode}
|
||||
isLoading={isLoading}
|
||||
leftIcon={<Icon as={FaQrcode} />}
|
||||
_hover={{ bg: "green.50" }}
|
||||
>
|
||||
{wechatStatus === WECHAT_STATUS.EXPIRED ? "点击刷新" : "获取二维码"}
|
||||
</Button>
|
||||
{wechatStatus === WECHAT_STATUS.EXPIRED && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
二维码已过期
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 扫码状态提示 */}
|
||||
{renderStatusText()}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -77,4 +77,4 @@ class ErrorBoundary extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export
|
||||
export default ErrorBoundary;
|
||||
Reference in New Issue
Block a user