feat: 调整注册逻辑
This commit is contained in:
34
src/App.js
34
src/App.js
@@ -9,7 +9,7 @@
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Visionware.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from "react";
|
||||
import React, { Suspense, useEffect } from "react";
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
|
||||
@@ -45,6 +45,7 @@ import { AuthProvider } from "contexts/AuthContext";
|
||||
|
||||
// Components
|
||||
import ProtectedRoute from "components/ProtectedRoute";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
|
||||
function AppContent() {
|
||||
const { colorMode } = useColorMode();
|
||||
@@ -152,11 +153,36 @@ function AppContent() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
// 全局错误处理:捕获未处理的 Promise rejection
|
||||
useEffect(() => {
|
||||
const handleUnhandledRejection = (event) => {
|
||||
console.error('未捕获的 Promise rejection:', event.reason);
|
||||
// 阻止默认的错误处理(防止崩溃)
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleError = (event) => {
|
||||
console.error('全局错误:', event.error);
|
||||
// 阻止默认的错误处理(防止崩溃)
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
window.addEventListener('error', handleError);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
window.removeEventListener('error', handleError);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ChakraProvider theme={theme}>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
<ErrorBoundary>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</ErrorBoundary>
|
||||
</ChakraProvider>
|
||||
);
|
||||
}
|
||||
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;
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
|
||||
// 导入认证相关页面
|
||||
import SignInIllustration from '../views/Authentication/SignIn/SignInIllustration';
|
||||
@@ -33,32 +34,34 @@ const AuthRoute = ({ children }) => {
|
||||
|
||||
export default function Auth() {
|
||||
return (
|
||||
<Box minH="100vh">
|
||||
<Routes>
|
||||
{/* 登录页面 */}
|
||||
<Route
|
||||
path="/signin"
|
||||
element={
|
||||
<AuthRoute>
|
||||
<SignInIllustration />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 注册页面 */}
|
||||
<Route
|
||||
path="/sign-up"
|
||||
element={
|
||||
<AuthRoute>
|
||||
<SignUpIllustration />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 默认重定向到登录页 */}
|
||||
<Route path="/" element={<Navigate to="/auth/signin" replace />} />
|
||||
<Route path="*" element={<Navigate to="/auth/signin" replace />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
<ErrorBoundary>
|
||||
<Box minH="100vh">
|
||||
<Routes>
|
||||
{/* 登录页面 */}
|
||||
<Route
|
||||
path="/signin"
|
||||
element={
|
||||
<AuthRoute>
|
||||
<SignInIllustration />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 注册页面 */}
|
||||
<Route
|
||||
path="/sign-up"
|
||||
element={
|
||||
<AuthRoute>
|
||||
<SignUpIllustration />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 默认重定向到登录页 */}
|
||||
<Route path="/" element={<Navigate to="/auth/signin" replace />} />
|
||||
<Route path="*" element={<Navigate to="/auth/signin" replace />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
121
src/services/authService.js
Normal file
121
src/services/authService.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// src/services/authService.js
|
||||
/**
|
||||
* 认证服务层 - 处理所有认证相关的 API 调用
|
||||
*/
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL;
|
||||
|
||||
/**
|
||||
* 统一的 API 请求处理
|
||||
* @param {string} url - 请求路径
|
||||
* @param {object} options - fetch 选项
|
||||
* @returns {Promise} - 响应数据
|
||||
*/
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include', // 包含 cookies
|
||||
});
|
||||
|
||||
// 检查响应是否为 JSON
|
||||
const contentType = response.headers.get('content-type');
|
||||
const isJson = contentType && contentType.includes('application/json');
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `HTTP error! status: ${response.status}`;
|
||||
if (isJson) {
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorData.message || errorMessage;
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse error response as JSON');
|
||||
}
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 安全地解析 JSON 响应
|
||||
if (isJson) {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse response as JSON:', parseError);
|
||||
throw new Error('服务器响应格式错误');
|
||||
}
|
||||
} else {
|
||||
throw new Error('服务器响应不是 JSON 格式');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Auth API request failed for ${url}:`, error);
|
||||
// 如果是网络错误,提供更友好的提示
|
||||
if (error.message === 'Failed to fetch' || error.name === 'TypeError') {
|
||||
throw new Error('网络连接失败,请检查网络设置');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const authService = {
|
||||
/**
|
||||
* 获取微信二维码授权链接
|
||||
* @returns {Promise<{auth_url: string, session_id: string}>}
|
||||
*/
|
||||
getWechatQRCode: async () => {
|
||||
return await apiRequest('/api/auth/wechat/qrcode');
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查微信扫码状态
|
||||
* @param {string} sessionId - 会话ID
|
||||
* @returns {Promise<{status: string, user_info?: object}>}
|
||||
*/
|
||||
checkWechatStatus: async (sessionId) => {
|
||||
return await apiRequest('/api/auth/wechat/check', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ session_id: sessionId }),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 使用微信 session 登录
|
||||
* @param {string} sessionId - 会话ID
|
||||
* @returns {Promise<{success: boolean, user?: object, token?: string}>}
|
||||
*/
|
||||
loginWithWechat: async (sessionId) => {
|
||||
return await apiRequest('/api/auth/login/wechat', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ session_id: sessionId }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 微信状态常量
|
||||
*/
|
||||
export const WECHAT_STATUS = {
|
||||
NONE: 'none',
|
||||
WAITING: 'waiting',
|
||||
SCANNED: 'scanned',
|
||||
AUTHORIZED: 'authorized',
|
||||
LOGIN_SUCCESS: 'login_success',
|
||||
REGISTER_SUCCESS: 'register_success',
|
||||
EXPIRED: 'expired',
|
||||
};
|
||||
|
||||
/**
|
||||
* 状态提示信息映射
|
||||
*/
|
||||
export const STATUS_MESSAGES = {
|
||||
[WECHAT_STATUS.WAITING]: '请使用微信扫码',
|
||||
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
|
||||
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
|
||||
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
|
||||
};
|
||||
|
||||
export default authService;
|
||||
@@ -17,14 +17,19 @@ import {
|
||||
IconButton,
|
||||
Link as ChakraLink,
|
||||
Center,
|
||||
useDisclosure
|
||||
useDisclosure,
|
||||
FormErrorMessage
|
||||
} from "@chakra-ui/react";
|
||||
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
|
||||
import { FaMobile, FaWeixin, FaLock, FaQrcode } from "react-icons/fa";
|
||||
import { useNavigate, Link, useLocation } from "react-router-dom";
|
||||
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";
|
||||
|
||||
// API配置
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
@@ -38,6 +43,7 @@ export default function SignInIllustration() {
|
||||
|
||||
// 页面状态
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
// 检查URL参数中的错误信息(微信登录失败时)
|
||||
useEffect(() => {
|
||||
@@ -45,38 +51,38 @@ export default function SignInIllustration() {
|
||||
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 = '登录失败,请重试';
|
||||
}
|
||||
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,
|
||||
});
|
||||
toast({
|
||||
title: "登录失败",
|
||||
description: errorMessage,
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// 清除URL参数
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
// 清除URL参数
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
}
|
||||
}, [location, toast]);
|
||||
|
||||
@@ -110,8 +116,8 @@ export default function SignInIllustration() {
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -194,17 +200,13 @@ export default function SignInIllustration() {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取微信授权URL
|
||||
const getWechatQRCode = async () => {
|
||||
|
||||
};
|
||||
|
||||
// 点击扫码,打开微信登录窗口
|
||||
const openWechatLogin = async() => {
|
||||
|
||||
const openWechatLogin = async () => {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
console.log("请求微信登录1...");
|
||||
// 获取微信二维码地址
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/wechat/qrcode`);
|
||||
|
||||
@@ -281,7 +283,7 @@ export default function SignInIllustration() {
|
||||
const credential = formData.phone;
|
||||
const authLoginType = 'phone';
|
||||
|
||||
if(useVerificationCode) { // 验证码登陆
|
||||
if (useVerificationCode) { // 验证码登陆
|
||||
if (!credential || !formData.verificationCode) {
|
||||
toast({
|
||||
title: "请填写完整信息",
|
||||
@@ -346,291 +348,144 @@ export default function SignInIllustration() {
|
||||
};
|
||||
|
||||
// 切换登录方式
|
||||
const handleChangeMethod = (status) => {
|
||||
if (!status) {
|
||||
const handleChangeMethod = () => {
|
||||
setUseVerificationCode(!useVerificationCode);
|
||||
// 切换到密码模式时清空验证码
|
||||
if (useVerificationCode) {
|
||||
setFormData(prev => ({ ...prev, verificationCode: "" }));
|
||||
}
|
||||
setUseVerificationCode(!useVerificationCode);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex minH="100vh" position="relative" overflow="hidden">
|
||||
{/* 流体波浪背景 */}
|
||||
<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)' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 背景 */}
|
||||
<AuthBackground />
|
||||
|
||||
{/* 主要内容 */}
|
||||
<Flex
|
||||
width="100%"
|
||||
align="center"
|
||||
justify="center"
|
||||
position="relative"
|
||||
zIndex={1}
|
||||
px={6}
|
||||
py={12}
|
||||
>
|
||||
<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="600px"
|
||||
backdropFilter="blur(20px)"
|
||||
border="1px solid rgba(255, 255, 255, 0.2)"
|
||||
>
|
||||
<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)">
|
||||
{/* 头部区域 */}
|
||||
<VStack spacing={6} mb={8}>
|
||||
<VStack spacing={2}>
|
||||
<Heading size="xl" color="gray.800" fontWeight="bold">
|
||||
欢迎回来
|
||||
</Heading>
|
||||
<Text color="gray.600" fontSize="md">
|
||||
登录价值前沿,继续您的投资之旅
|
||||
</Text>
|
||||
</VStack>
|
||||
<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>
|
||||
|
||||
{/* 登录表单 */}
|
||||
{/* setLoginType */}
|
||||
<VStack spacing={2} align="stretch">
|
||||
<HStack justify="center">
|
||||
{/* 传统登录 */}
|
||||
<form onSubmit={handleTraditionalLogin}>
|
||||
<VStack spacing={4}>
|
||||
<HStack spacing={2} width="100%" align="center"> {/* 设置 HStack 宽度为 100% */}
|
||||
<Text fontSize="md" fontWeight="bold" color="gray.700" minWidth="70px" mr={2} noOfLines={1} overflow="hidden" textOverflow="ellipsis">
|
||||
账号 :
|
||||
</Text>
|
||||
<FormControl isRequired flex="1 1 auto">
|
||||
<InputGroup>
|
||||
<Input
|
||||
name={"phone"}
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder={"请输入手机号"}
|
||||
size="lg"
|
||||
borderRadius="lg"
|
||||
bg="gray.50"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
_focus={{
|
||||
borderColor: "blue.500",
|
||||
boxShadow: "0 0 0 1px #667eea"
|
||||
}}
|
||||
/>
|
||||
<InputRightElement pointerEvents="none">
|
||||
<Icon as={FaMobile} color="gray.400" />
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
|
||||
{/* 密码输入框 */}
|
||||
{useVerificationCode ? (
|
||||
// 验证码输入框
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="md" fontWeight="bold" color={"gray.700"} minWidth="80px">验证码:</Text>
|
||||
<VStack spacing={3} align="stretch">
|
||||
<HStack>
|
||||
<FormControl isRequired flex="1 1 auto">
|
||||
<InputGroup size="lg">
|
||||
<Input
|
||||
name="verificationCode"
|
||||
value={formData.verificationCode}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入验证码"
|
||||
borderRadius="lg"
|
||||
bg="gray.50"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
_focus={{
|
||||
borderColor: "green.500",
|
||||
boxShadow: "0 0 0 1px #48bb78"
|
||||
}}
|
||||
maxLength={6}
|
||||
/>
|
||||
{/* <InputRightElement>
|
||||
<Icon as={FaCode} color="gray.400"/>
|
||||
</InputRightElement> */}
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
<Button
|
||||
flex="0 0 auto" // 让按钮大小根据内容自适应
|
||||
size="md"
|
||||
colorScheme="green"
|
||||
variant="outline"
|
||||
onClick={sendVerificationCode}
|
||||
isLoading={sendingCode}
|
||||
isDisabled={verificationCodeSent && countdown > 0}
|
||||
borderRadius="lg"
|
||||
fontSize="sm" // 调整字体大小
|
||||
whiteSpace="nowrap" // 防止文本换行
|
||||
minWidth="120px" // 设置按钮最小宽度
|
||||
>
|
||||
{sendingCode ? "发送中..." : verificationCodeSent && countdown > 0 ? `${countdown}s` : "发送验证码"}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
):(
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="md" fontWeight="bold" color="gray.700" minWidth="70px" mr={2} noOfLines={1} overflow="hidden" textOverflow="ellipsis">
|
||||
密码:
|
||||
</Text>
|
||||
<FormControl isRequired flex="1 1 auto">
|
||||
<InputGroup size="lg">
|
||||
<Input
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入密码"
|
||||
borderRadius="lg"
|
||||
bg="gray.50"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
_focus={{
|
||||
borderColor: "blue.500",
|
||||
boxShadow: "0 0 0 1px #667eea"
|
||||
}}
|
||||
/>
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
aria-label={showPassword ? "隐藏密码" : "显示密码"}
|
||||
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<HStack justify="space-between" width="100%">
|
||||
<HStack spacing={1} as={Link} to="/auth/sign-up">
|
||||
<Text fontSize="sm" color="gray.600">还没有账号,</Text>
|
||||
<Text fontSize="sm" color="blue.500" fontWeight="bold">去注册</Text>
|
||||
</HStack>
|
||||
<ChakraLink href="#" fontSize="sm" color="blue.500" fontWeight="bold" onClick={handleChangeMethod}>
|
||||
{useVerificationCode ? '密码登陆' : '验证码登陆'}
|
||||
</ChakraLink>
|
||||
</HStack>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
width="100%"
|
||||
size="lg"
|
||||
colorScheme="green"
|
||||
color="white"
|
||||
borderRadius="lg"
|
||||
_hover={{
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: "lg"
|
||||
}}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
{/* 密码/验证码输入框 */}
|
||||
{useVerificationCode ? (
|
||||
<VerificationCodeInput
|
||||
value={formData.verificationCode}
|
||||
onChange={handleInputChange}
|
||||
onSendCode={sendVerificationCode}
|
||||
countdown={countdown}
|
||||
isLoading={isLoading}
|
||||
loadingText="登录中..."
|
||||
fontWeight="bold"
|
||||
cursor={"pointer"}
|
||||
>
|
||||
<Icon as={FaLock} mr={2} />登录
|
||||
</Button>
|
||||
</VStack>
|
||||
</form>
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 微信登录 - 简化版 */}
|
||||
<VStack spacing={6}>
|
||||
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
|
||||
<VStack spacing={6}>
|
||||
<VStack spacing={2}>
|
||||
<Text fontSize="lg" fontWeight="bold" color={"gray.700"}>
|
||||
微信扫一扫
|
||||
</Text>
|
||||
</VStack>
|
||||
<Icon as={FaQrcode} w={20} h={20} color={"green.500"} />
|
||||
{/* isLoading={isLoading || !wechatAuthUrl} */}
|
||||
<Button
|
||||
colorScheme="green"
|
||||
size="lg"
|
||||
leftIcon={<Icon as={FaWeixin} />}
|
||||
onClick={openWechatLogin}
|
||||
_hover={{ transform: "translateY(-2px)", boxShadow: "lg" }}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
|
||||
>
|
||||
扫码登录
|
||||
</Button>
|
||||
</VStack>
|
||||
</Center>
|
||||
|
||||
|
||||
<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>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</form>
|
||||
</Box>
|
||||
{/* 右侧:微信登陆 - 20% 宽度 */}
|
||||
<Box flex="1">
|
||||
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
|
||||
<VStack spacing={6}>
|
||||
<VStack spacing={2}>
|
||||
<Text fontSize="lg" fontWeight="bold" color={"gray.700"}>
|
||||
微信扫一扫
|
||||
</Text>
|
||||
</VStack>
|
||||
<Icon as={FaQrcode} w={20} h={20} color={"green.500"} />
|
||||
{/* isLoading={isLoading || !wechatAuthUrl} */}
|
||||
<Button
|
||||
colorScheme="green"
|
||||
size="lg"
|
||||
leftIcon={<Icon as={FaWeixin} />}
|
||||
onClick={openWechatLogin}
|
||||
_hover={{ transform: "translateY(-2px)", boxShadow: "lg" }}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
|
||||
>
|
||||
扫码登录
|
||||
</Button>
|
||||
</VStack>
|
||||
</Center>
|
||||
</Box>
|
||||
</HStack>
|
||||
|
||||
{/* 底部链接 */}
|
||||
<VStack spacing={4} mt={6}>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user