Files
vf_react/src/components/Auth/WechatRegister.js
2025-10-16 11:24:24 +08:00

411 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<Text fontSize="xs" color="gray.500">
{STATUS_MESSAGES[wechatStatus]}
</Text>
);
};
return (
<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="300"
height="350"
style={{
borderRadius: '8px',
border: 'none',
transform: `scale(${scale})`,
transformOrigin: 'center center'
}}
/>
</Box>
{/* {renderStatusText()} */}
</>
) : (
<>
<Text fontSize="lg" fontWeight="bold" color="gray.700" whiteSpace="nowrap">
微信扫码
</Text>
<Box
position="relative"
width="150px"
height="100px"
maxWidth="100%"
display="flex"
alignItems="center"
justifyContent="center"
overflow="hidden"
>
{/* 灰色二维码底图 - 始终显示 */}
<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>
);
}