Compare commits

...

3 Commits

Author SHA1 Message Date
zdl
4ac6c4892e feat: 修复用户登陆模块 2025-10-16 15:23:50 +08:00
zdl
98ea8f2427 feat: 调整微信登陆UI 2025-10-16 11:24:24 +08:00
zdl
7c166f7186 feat: 手机验证码调试 2025-10-16 10:09:15 +08:00
9 changed files with 230 additions and 113 deletions

View File

@@ -219,5 +219,14 @@ module.exports = {
devMiddleware: {
writeToDisk: false,
},
// 代理配置:将 /api 请求代理到后端服务器
proxy: {
'/api': {
target: 'http://49.232.185.254:5001',
changeOrigin: true,
secure: false,
logLevel: 'debug',
},
},
},
};

View File

@@ -37,7 +37,7 @@ import WechatRegister from './WechatRegister';
// API配置
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = isProduction ? "" : "http://49.232.185.254:5000";
const API_BASE_URL = isProduction ? "" : "http://49.232.185.254:5001";
// 统一配置对象
const AUTH_CONFIG = {
@@ -54,7 +54,7 @@ const AUTH_CONFIG = {
// API配置
api: {
endpoint: '/api/auth/register/phone',
purpose: 'register',
purpose: 'login', // ⚡ 统一使用 'login' 模式
},
// 功能开关
@@ -159,6 +159,7 @@ export default function AuthFormContent() {
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
credential,
type: 'phone',

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from "react";
import {
Box,
Button,
@@ -14,6 +14,7 @@ import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/auth
// 配置常量
const POLL_INTERVAL = 2000; // 轮询间隔2秒
const BACKUP_POLL_INTERVAL = 3000; // 备用轮询间隔3秒
const QR_CODE_TIMEOUT = 300000; // 二维码超时5分钟
export default function WechatRegister() {
@@ -22,11 +23,14 @@ export default function WechatRegister() {
const [wechatSessionId, setWechatSessionId] = useState("");
const [wechatStatus, setWechatStatus] = useState(WECHAT_STATUS.NONE);
const [isLoading, setIsLoading] = useState(false);
const [scale, setScale] = useState(1); // iframe 缩放比例
// 使用 useRef 管理定时器,避免闭包问题和内存泄漏
const pollIntervalRef = useRef(null);
const backupPollIntervalRef = useRef(null); // 备用轮询定时器
const timeoutRef = useRef(null);
const isMountedRef = useRef(true); // 追踪组件挂载状态
const containerRef = useRef(null); // 容器DOM引用
const navigate = useNavigate();
const toast = useToast();
@@ -65,6 +69,10 @@ export default function WechatRegister() {
clearInterval(pollIntervalRef.current);
pollIntervalRef.current = null;
}
if (backupPollIntervalRef.current) {
clearInterval(backupPollIntervalRef.current);
backupPollIntervalRef.current = null;
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
@@ -192,6 +200,7 @@ export default function WechatRegister() {
try {
setIsLoading(true);
// 生产环境:调用真实 API
const response = await authService.getWechatQRCode();
// 检查组件是否已卸载
@@ -201,13 +210,13 @@ export default function WechatRegister() {
if (!response) {
throw new Error('服务器无响应');
}
if (!response.auth_url || !response.session_id) {
throw new Error('获取二维码失败:响应数据不完整');
if (response.code !== 0) {
throw new Error('获取二维码失败');
}
setWechatAuthUrl(response.auth_url);
setWechatSessionId(response.session_id);
setWechatAuthUrl(response.data.auth_url);
setWechatSessionId(response.data.session_id);
setWechatStatus(WECHAT_STATUS.WAITING);
// 启动轮询检查扫码状态
@@ -236,6 +245,80 @@ export default function WechatRegister() {
};
}, [clearTimers]);
/**
* 备用轮询机制 - 防止丢失状态
* 每3秒检查一次仅在获取到二维码URL且状态为waiting时执行
*/
useEffect(() => {
// 只在有auth_url、session_id且状态为waiting时启动备用轮询
if (wechatAuthUrl && wechatSessionId && wechatStatus === WECHAT_STATUS.WAITING) {
console.log('备用轮询:启动备用轮询机制');
backupPollIntervalRef.current = setInterval(() => {
try {
if (wechatStatus === WECHAT_STATUS.WAITING && isMountedRef.current) {
console.log('备用轮询:检查微信状态');
// 添加 .catch() 静默处理异步错误,防止被 ErrorBoundary 捕获
checkWechatStatus().catch(error => {
console.warn('备用轮询检查失败(静默处理):', error);
});
}
} catch (error) {
// 捕获所有同步错误,防止被 ErrorBoundary 捕获
console.warn('备用轮询执行出错(静默处理):', error);
}
}, BACKUP_POLL_INTERVAL);
}
// 清理备用轮询
return () => {
if (backupPollIntervalRef.current) {
clearInterval(backupPollIntervalRef.current);
backupPollIntervalRef.current = null;
}
};
}, [wechatAuthUrl, wechatSessionId, wechatStatus, checkWechatStatus]);
/**
* 测量容器尺寸并计算缩放比例
*/
useLayoutEffect(() => {
// 微信授权页面的原始尺寸
const ORIGINAL_WIDTH = 600;
const ORIGINAL_HEIGHT = 800;
const calculateScale = () => {
if (containerRef.current) {
const { width, height } = containerRef.current.getBoundingClientRect();
// 计算宽高比例,取较小值确保完全适配
const scaleX = width / ORIGINAL_WIDTH;
const scaleY = height / ORIGINAL_HEIGHT;
const newScale = Math.min(scaleX, scaleY, 1.0); // 最大不超过1.0
// 设置最小缩放比例为0.3,避免过小
setScale(Math.max(newScale, 0.3));
}
};
// 初始计算
calculateScale();
// 使用 ResizeObserver 监听容器尺寸变化
const resizeObserver = new ResizeObserver(() => {
calculateScale();
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
// 清理
return () => {
resizeObserver.disconnect();
};
}, [wechatStatus]); // 当状态变化时重新计算
/**
* 渲染状态提示文本
*/
@@ -246,96 +329,123 @@ export default function WechatRegister() {
return (
<Text fontSize="xs" color="gray.500">
{STATUS_MESSAGES[wechatStatus] || STATUS_MESSAGES[WECHAT_STATUS.WAITING]}
{STATUS_MESSAGES[wechatStatus]}
</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"
>
{/* 灰色二维码底图 - 始终显示 */}
{wechatStatus === WECHAT_STATUS.WAITING ? (
<Box position="relative" width="96px" height="96px">
<VStack spacing={2} display="flex" alignItems="center" justifyContent="center">
{wechatStatus === WECHAT_STATUS.WAITING ? (
<>
<Text fontSize="lg" fontWeight="bold" color="gray.700" whiteSpace="nowrap">
微信扫码
</Text>
<Box
ref={containerRef}
position="relative"
width="150px"
height="100px"
maxWidth="100%"
display="flex"
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<iframe
src={wechatAuthUrl}
width="96"
height="96"
style={{ borderRadius: '8px', border: 'none' }}
title="微信扫码登录"
width="300"
height="350"
style={{
borderRadius: '8px',
border: 'none',
transform: `scale(${scale})`,
transformOrigin: 'center center'
}}
/>
</Box>
) : (
<Icon as={FaQrcode} w={24} h={24} color="gray.300" />
)}
{/* {renderStatusText()} */}
</>
) : (
<>
<Text fontSize="lg" fontWeight="bold" color="gray.700" whiteSpace="nowrap">
微信扫码
</Text>
{/* 加载动画 */}
{isLoading && (
<Box
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
position="relative"
width="150px"
height="100px"
maxWidth="100%"
display="flex"
alignItems="center"
justifyContent="center"
overflow="hidden"
>
<Spinner
size="lg"
color="green.500"
thickness="4px"
/>
</Box>
)}
{/* 灰色二维码底图 - 始终显示 */}
<Icon as={FaQrcode} w={24} h={24} color="gray.300" />
{/* 显示获取/刷新二维码按钮 */}
{(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" }}
{/* 加载动画 */}
{isLoading && (
<Box
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
display="flex"
alignItems="center"
justifyContent="center"
>
{wechatStatus === WECHAT_STATUS.EXPIRED ? "点击刷新" : "获取二维码"}
</Button>
{wechatStatus === WECHAT_STATUS.EXPIRED && (
<Text fontSize="xs" color="gray.500">
二维码已过期
</Text>
)}
</VStack>
</Box>
)}
</Box>
<Spinner
size="lg"
color="green.500"
thickness="4px"
/>
</Box>
)}
{/* 扫码状态提示 */}
{renderStatusText()}
{/* 显示获取/刷新二维码按钮 */}
{(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>
);
}

View File

@@ -36,22 +36,10 @@ const ProtectedRoute = ({ children }) => {
);
}
// 未登录时显示占位符(弹窗会自动打开
// 未登录时,渲染子组件 + 自动打开弹窗(通过 useEffect
// 弹窗会在 useEffect 中自动触发,页面正常显示
if (!isAuthenticated || !user) {
return (
<Box
height="100vh"
display="flex"
alignItems="center"
justifyContent="center"
bg="gray.50"
>
<VStack spacing={4}>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text fontSize="lg" color="gray.600">请先登录...</Text>
</VStack>
</Box>
);
return children;
}
// 已登录,渲染子组件

View File

@@ -5,7 +5,7 @@ import { useToast } from '@chakra-ui/react';
// API基础URL配置
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL || "http://49.232.185.254:5000";
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL || "http://49.232.185.254:5001";
// 创建认证上下文
const AuthContext = createContext();

View File

@@ -22,7 +22,7 @@ export const useAuth = () => {
// 认证提供者组件
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false); // ⚡ 改为 false不阻塞首屏渲染
const [isLoading, setIsLoading] = useState(true); // ⚡ 串行执行,阻塞渲染直到 Session 检查完成
const [isAuthenticated, setIsAuthenticated] = useState(false);
const navigate = useNavigate();
const toast = useToast();
@@ -66,8 +66,10 @@ export const AuthProvider = ({ children }) => {
// 网络错误或超时,设置为未登录状态
setUser(null);
setIsAuthenticated(false);
} finally {
// ⚡ Session 检查完成后,停止加载状态
setIsLoading(false);
}
// ⚡ 移除 finally 中的 setIsLoading(false),不再阻塞渲染
};
// ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染

View File

@@ -1,5 +1,7 @@
// src/contexts/AuthModalContext.js
import { createContext, useContext, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from './AuthContext';
const AuthModalContext = createContext();
@@ -19,6 +21,9 @@ export const useAuthModal = () => {
* 管理统一的认证弹窗状态(登录/注册合并)
*/
export const AuthModalProvider = ({ children }) => {
const navigate = useNavigate();
const { isAuthenticated } = useAuth();
// 弹窗状态(统一的认证弹窗)
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
@@ -41,12 +46,18 @@ export const AuthModalProvider = ({ children }) => {
/**
* 关闭认证弹窗
* 如果用户未登录,跳转到首页
*/
const closeModal = useCallback(() => {
setIsAuthModalOpen(false);
setRedirectUrl(null);
setOnSuccessCallback(null);
}, []);
// ⭐ 如果用户关闭弹窗时仍未登录,跳转到首页
if (!isAuthenticated) {
navigate('/home');
}
}, [isAuthenticated, navigate]);
/**
* 登录/注册成功处理
@@ -62,17 +73,12 @@ export const AuthModalProvider = ({ children }) => {
}
}
// 如果有重定向URL跳转
if (redirectUrl) {
// 使用 window.location.href 确保完整刷新页面状态
setTimeout(() => {
window.location.href = redirectUrl;
}, 500); // 延迟500ms让用户看到成功提示
}
// 关闭弹窗
closeModal();
}, [onSuccessCallback, redirectUrl, closeModal]);
// ⭐ 登录成功后,只关闭弹窗,留在当前页面(不跳转
// 移除了原有的 redirectUrl 跳转逻辑
setIsAuthModalOpen(false);
setRedirectUrl(null);
setOnSuccessCallback(null);
}, [onSuccessCallback]);
/**
* 提供给子组件的上下文值

View File

@@ -34,7 +34,7 @@ import WechatRegister from "../../../components/Auth/WechatRegister";
// API配置
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = isProduction ? "" : "http://49.232.185.254:5000";
const API_BASE_URL = isProduction ? "" : "http://49.232.185.254:5001";
export default function SignInIllustration() {
const navigate = useNavigate();
@@ -179,6 +179,7 @@ export default function SignInIllustration() {
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
credential,
type,

View File

@@ -119,7 +119,7 @@ export default function SettingsPage() {
try {
const API_BASE_URL = process.env.NODE_ENV === 'production'
? ""
: process.env.REACT_APP_API_URL || "http://49.232.185.254:5000";
: process.env.REACT_APP_API_URL || "http://49.232.185.254:5001";
const response = await fetch(`${API_BASE_URL}/api/account/password-status`, {
method: 'GET',
@@ -188,7 +188,7 @@ export default function SettingsPage() {
// 调用后端API修改密码
const API_BASE_URL = process.env.NODE_ENV === 'production'
? ""
: process.env.REACT_APP_API_URL || "http://49.232.185.254:5000";
: process.env.REACT_APP_API_URL || "http://49.232.185.254:5001";
const response = await fetch(`${API_BASE_URL}/api/account/change-password`, {
method: 'POST',