411 lines
14 KiB
JavaScript
411 lines
14 KiB
JavaScript
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>
|
||
);
|
||
}
|