import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from "react"; import { Box, Button, VStack, HStack, Center, Text, Heading, Icon, useToast, Spinner } from "@chakra-ui/react"; import { FaQrcode } from "react-icons/fa"; import { FiAlertCircle } from "react-icons/fi"; import { useNavigate } from "react-router-dom"; import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService"; import { useAuthModal } from "../../hooks/useAuthModal"; import { useAuth } from "../../contexts/AuthContext"; import { logger } from "../../utils/logger"; import { useAuthEvents } from "../../hooks/useAuthEvents"; // 配置常量 const POLL_INTERVAL = 2000; // 轮询间隔:2秒 const BACKUP_POLL_INTERVAL = 3000; // 备用轮询间隔:3秒 const QR_CODE_TIMEOUT = 300000; // 二维码超时:5分钟 /** * 获取状态文字颜色 */ const getStatusColor = (status) => { switch(status) { case WECHAT_STATUS.WAITING: return "gray.600"; // ✅ 灰色文字 case WECHAT_STATUS.SCANNED: return "green.600"; // ✅ 绿色文字 case WECHAT_STATUS.AUTHORIZED: return "green.600"; // ✅ 绿色文字 case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字 case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字 case WECHAT_STATUS.REGISTER_SUCCESS: return "green.600"; case WECHAT_STATUS.AUTH_DENIED: return "red.600"; // ✅ 红色文字 case WECHAT_STATUS.AUTH_FAILED: return "red.600"; // ✅ 红色文字 default: return "gray.600"; } }; /** * 获取状态文字 */ const getStatusText = (status) => { return STATUS_MESSAGES[status] || "点击按钮获取二维码"; }; export default function WechatRegister() { // 获取关闭弹窗方法 const { closeModal } = useAuthModal(); const { refreshSession } = useAuth(); // 事件追踪 const authEvents = useAuthEvents({ component: 'WechatRegister', isMobile: false // 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 backupPollIntervalRef = useRef(null); // 备用轮询定时器 const timeoutRef = useRef(null); const isMountedRef = useRef(true); // 追踪组件挂载状态 const containerRef = useRef(null); // 容器DOM引用 const sessionIdRef = useRef(null); // 存储最新的 sessionId,避免闭包陷阱 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]); /** * 清理所有定时器 * 注意:不清理 sessionIdRef,因为 startPolling 时也会调用此函数 */ const clearTimers = useCallback(() => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } if (backupPollIntervalRef.current) { clearInterval(backupPollIntervalRef.current); backupPollIntervalRef.current = null; } if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }, []); /** * 处理登录成功 */ const handleLoginSuccess = useCallback(async (sessionId, status) => { try { logger.info('WechatRegister', '开始调用登录接口', { sessionId: sessionId.substring(0, 8) + '...', status }); const response = await authService.loginWithWechat(sessionId); logger.info('WechatRegister', '登录接口返回', { success: response?.success, hasUser: !!response?.user }); if (response?.success) { // 追踪微信登录成功 authEvents.trackLoginSuccess( response.user, 'wechat', response.isNewUser || false ); // 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 ? "登录成功" : "欢迎回来!" ); // 刷新 AuthContext 状态 await refreshSession(); // 关闭认证弹窗,留在当前页面 closeModal(); } else { throw new Error(response?.error || '登录失败'); } } catch (error) { // 追踪微信登录失败 authEvents.trackLoginFailed('wechat', 'api', error.message || '登录失败', { session_id: sessionId?.substring(0, 8) + '...', status: status }); logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId }); showError("登录失败", error.message || "请重试"); } }, [showSuccess, showError, closeModal, refreshSession, authEvents]); /** * 检查微信扫码状态 * 使用 sessionIdRef.current 避免闭包陷阱 */ const checkWechatStatus = useCallback(async () => { // 检查组件是否已卸载,使用 ref 获取最新的 sessionId if (!isMountedRef.current || !sessionIdRef.current) { logger.debug('WechatRegister', 'checkWechatStatus 跳过', { isMounted: isMountedRef.current, hasSessionId: !!sessionIdRef.current }); return; } const currentSessionId = sessionIdRef.current; logger.debug('WechatRegister', '检查微信状态', { sessionId: currentSessionId }); try { const response = await authService.checkWechatStatus(currentSessionId); // 安全检查:确保 response 存在且包含 status if (!response || typeof response.status === 'undefined') { logger.warn('WechatRegister', '微信状态检查返回无效数据', { response }); return; } const { status } = response; logger.debug('WechatRegister', '微信状态', { status }); logger.debug('WechatRegister', '检测到微信状态', { sessionId: wechatSessionId.substring(0, 8) + '...', status, userInfo: response.user_info }); // 组件卸载后不再更新状态 if (!isMountedRef.current) return; // 追踪状态变化 if (wechatStatus !== status) { authEvents.trackWechatStatusChanged(currentSessionId, wechatStatus, status); // 特别追踪扫码事件 if (status === WECHAT_STATUS.SCANNED) { authEvents.trackWechatQRScanned(currentSessionId); } } setWechatStatus(status); // 处理成功状态 if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) { logger.info('WechatRegister', '检测到登录成功状态,停止轮询', { status }); clearTimers(); // 停止轮询 sessionIdRef.current = null; // 清理 sessionId await handleLoginSuccess(currentSessionId, status); } // 处理过期状态 else if (status === WECHAT_STATUS.EXPIRED) { // 追踪二维码过期 authEvents.trackWechatQRExpired(currentSessionId, QR_CODE_TIMEOUT / 1000); clearTimers(); sessionIdRef.current = null; // 清理 sessionId if (isMountedRef.current) { toast({ title: "授权已过期", description: "请重新获取授权", status: "warning", duration: 3000, isClosable: true, }); } } // 处理用户拒绝授权 else if (status === WECHAT_STATUS.AUTH_DENIED) { clearTimers(); if (isMountedRef.current) { toast({ title: "授权已取消", description: "您已取消微信授权登录", status: "warning", duration: 3000, isClosable: true, }); } } // 处理授权失败 else if (status === WECHAT_STATUS.AUTH_FAILED) { clearTimers(); if (isMountedRef.current) { const errorMsg = response.error || "授权过程出现错误"; toast({ title: "授权失败", description: errorMsg, status: "error", duration: 5000, isClosable: true, }); } } } catch (error) { logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId }); // 轮询过程中的错误不显示给用户,避免频繁提示 // 但如果错误持续发生,停止轮询避免无限重试 if (error.message.includes('网络连接失败')) { clearTimers(); sessionIdRef.current = null; // 清理 sessionId if (isMountedRef.current) { toast({ title: "网络连接失败", description: "请检查网络后重试", status: "error", duration: 3000, isClosable: true, }); } } } }, [handleLoginSuccess, clearTimers, toast]); /** * 启动轮询 */ const startPolling = useCallback(() => { logger.debug('WechatRegister', '启动轮询', { sessionId: sessionIdRef.current, interval: POLL_INTERVAL }); // 清理旧的定时器 clearTimers(); // 启动轮询 pollIntervalRef.current = setInterval(() => { checkWechatStatus(); }, POLL_INTERVAL); // 设置超时 timeoutRef.current = setTimeout(() => { logger.debug('WechatRegister', '二维码超时'); clearTimers(); sessionIdRef.current = null; // 清理 sessionId setWechatStatus(WECHAT_STATUS.EXPIRED); }, QR_CODE_TIMEOUT); }, [checkWechatStatus, clearTimers]); /** * 获取微信二维码 */ const getWechatQRCode = useCallback(async () => { try { setIsLoading(true); // 追踪用户选择微信登录(首次或刷新) const isRefresh = Boolean(wechatSessionId); if (isRefresh) { const oldSessionId = wechatSessionId; authEvents.trackWechatLoginInitiated('qr_refresh'); // 稍后会在成功时追踪刷新事件 } else { authEvents.trackWechatLoginInitiated('qr_area'); } // 生产环境:调用真实 API const response = await authService.getWechatQRCode(); // 检查组件是否已卸载 if (!isMountedRef.current) return; // 安全检查:确保响应包含必要字段 if (!response) { throw new Error('服务器无响应'); } if (response.code !== 0) { throw new Error(response.message || '获取二维码失败'); } // 追踪二维码显示 (首次或刷新) if (isRefresh) { authEvents.trackWechatQRRefreshed(wechatSessionId, response.data.session_id); } else { authEvents.trackWechatQRDisplayed(response.data.session_id, response.data.auth_url); } // 同时更新 ref 和 state,确保轮询能立即读取到最新值 sessionIdRef.current = response.data.session_id; setWechatAuthUrl(response.data.auth_url); setWechatSessionId(response.data.session_id); setWechatStatus(WECHAT_STATUS.WAITING); logger.debug('WechatRegister', '获取二维码成功', { sessionId: response.data.session_id, authUrl: response.data.auth_url }); // 启动轮询检查扫码状态 startPolling(); } catch (error) { // 追踪获取二维码失败 authEvents.trackError('api', error.message || '获取二维码失败', { context: 'get_wechat_qrcode' }); logger.error('WechatRegister', 'getWechatQRCode', error); if (isMountedRef.current) { showError("获取微信授权失败", error.message || "请稍后重试"); } } finally { if (isMountedRef.current) { setIsLoading(false); } } }, [startPolling, showError, wechatSessionId, authEvents]); /** * 安全的按钮点击处理,确保所有错误都被捕获,防止被 ErrorBoundary 捕获 */ const handleGetQRCodeClick = useCallback(async () => { try { await getWechatQRCode(); } catch (error) { // 错误已经在 getWechatQRCode 中处理,这里只需要防止未捕获的 Promise rejection logger.error('WechatRegister', 'handleGetQRCodeClick', error); } }, [getWechatQRCode]); /** * 组件卸载时清理定时器和标记组件状态 */ useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; clearTimers(); sessionIdRef.current = null; // 清理 sessionId }; }, [clearTimers]); /** * 测量容器尺寸并计算缩放比例 */ useLayoutEffect(() => { // 微信授权页面的原始尺寸(需要与iframe实际尺寸匹配) const ORIGINAL_WIDTH = 300; // ✅ 修正:与iframe width匹配 const ORIGINAL_HEIGHT = 350; // ✅ 修正:与iframe height匹配 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.NONE ? ( /* 已获取二维码:显示iframe */