import React, { useState, useEffect, useLayoutEffect, 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); const [scale, setScale] = useState(1); // iframe 缩放比例 // 使用 useRef 管理定时器,避免闭包问题和内存泄漏 const pollIntervalRef = useRef(null); const timeoutRef = useRef(null); const isMountedRef = useRef(true); // 追踪组件挂载状态 const containerRef = useRef(null); // 容器DOM引用 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); // 生产环境:调用真实 API const response = await authService.getWechatQRCode(); // 检查组件是否已卸载 if (!isMountedRef.current) return; // 安全检查:确保响应包含必要字段 if (!response) { throw new Error('服务器无响应'); } if (response.code !== 0) { throw new Error('获取二维码失败'); } setWechatAuthUrl(response.data.auth_url); setWechatSessionId(response.data.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]); /** * 测量容器尺寸并计算缩放比例 */ 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]); // 当状态变化时重新计算 /** * 渲染状态提示文本 */ const renderStatusText = () => { if (!wechatAuthUrl || wechatStatus === WECHAT_STATUS.NONE || wechatStatus === WECHAT_STATUS.EXPIRED) { return null; } return ( {STATUS_MESSAGES[wechatStatus]} ); }; return ( {wechatStatus === WECHAT_STATUS.WAITING ? ( <> 微信扫码