feat(Auth): 微信扫码登录二维码自动获取与轮询控制优化

- WechatRegister: 使用 forwardRef + useImperativeHandle 暴露方法
- 自动获取二维码:打开弹窗、切换到微信Tab时自动获取
- 轮询控制:切换到手机登录时停止轮询并重置状态
- 修复闭包陷阱:使用 isLoadingRef 替代 isLoading 状态判断
- 新增 iframe 加载状态反馈和 2 分钟过期预警提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-30 18:05:09 +08:00
parent 97c10bf2cc
commit 70fdad9751
2 changed files with 112 additions and 26 deletions

View File

@@ -182,6 +182,7 @@ export default function AuthFormContent() {
const config = AUTH_CONFIG;
const isMountedRef = useRef(true);
const wechatRef = useRef(null);
// Tab 状态: 'wechat' | 'phone'
const [activeTab, setActiveTab] = useState('wechat');
@@ -203,6 +204,14 @@ export default function AuthFormContent() {
// 切换 Tab
const handleTabChange = (tab) => {
// 切换到手机登录时,停止微信轮询
if (tab === 'phone' && wechatRef.current) {
wechatRef.current.stopPolling();
}
// 切换到微信登录时,自动获取二维码
if (tab === 'wechat' && wechatRef.current) {
wechatRef.current.fetchQRCode();
}
setActiveTab(tab);
authEvents.trackLoginPageViewed(); // 追踪切换
};
@@ -381,7 +390,19 @@ export default function AuthFormContent() {
useEffect(() => {
isMountedRef.current = true;
authEvents.trackLoginPageViewed();
return () => { isMountedRef.current = false; };
// 组件挂载时,如果默认 Tab 是微信登录,自动获取二维码
// 使用 setTimeout 确保 ref 已经绑定
const timer = setTimeout(() => {
if (activeTab === 'wechat' && wechatRef.current) {
wechatRef.current.fetchQRCode();
}
}, 100);
return () => {
isMountedRef.current = false;
clearTimeout(timer);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -459,7 +480,7 @@ export default function AuthFormContent() {
// 渲染微信登录区域
const renderWechatLogin = () => (
<div>
<WechatRegister />
<WechatRegister ref={wechatRef} />
<div style={{ ...styles.privacyText, marginTop: '16px' }}>
扫码即表示同意{" "}
<Link

View File

@@ -1,6 +1,6 @@
// src/components/Auth/WechatRegister.js
// 微信登录组件 - 黑金主题
import React, { useState, useEffect, useRef, useCallback } from "react";
import React, { useState, useEffect, useRef, useCallback, forwardRef, useImperativeHandle } from "react";
import { Typography, Button, message } from 'antd';
import { QrcodeOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
@@ -13,7 +13,8 @@ const { Title, Text } = Typography;
// 配置常量
const POLL_INTERVAL = 2000;
const QR_CODE_TIMEOUT = 300000;
const QR_CODE_TIMEOUT = 300000; // 5 分钟过期
const QR_CODE_WARNING_TIME = 120000; // 2 分钟时提示即将过期
// 黑金主题样式
const THEME = {
@@ -119,6 +120,22 @@ const styles = {
fontSize: '12px',
marginTop: '4px',
},
iframeLoading: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(255, 255, 255, 0.95)',
zIndex: 1,
},
loadingText: {
color: THEME.goldPrimary,
fontSize: '14px',
},
};
const getStatusColor = (status) => {
@@ -139,7 +156,7 @@ const getStatusColor = (status) => {
const getStatusText = (status) => STATUS_MESSAGES[status] || "点击按钮获取二维码";
export default function WechatRegister({ subtitle }) {
const WechatRegister = forwardRef(function WechatRegister({ subtitle }, ref) {
const { closeModal } = useAuthModal();
const { refreshSession } = useAuth();
const authEvents = useAuthEvents({ component: 'WechatRegister', isMobile: false });
@@ -148,13 +165,16 @@ export default function WechatRegister({ subtitle }) {
const [wechatSessionId, setWechatSessionId] = useState("");
const [wechatStatus, setWechatStatus] = useState(WECHAT_STATUS.NONE);
const [isLoading, setIsLoading] = useState(false);
const [iframeLoading, setIframeLoading] = useState(true);
const pollIntervalRef = useRef(null);
const timeoutRef = useRef(null);
const warningTimeoutRef = useRef(null); // 过期预警定时器
const isMountedRef = useRef(true);
const containerRef = useRef(null);
const sessionIdRef = useRef(null);
const wechatStatusRef = useRef(WECHAT_STATUS.NONE);
const isLoadingRef = useRef(false); // 用于防止重复请求(避免闭包陷阱)
const clearTimers = useCallback(() => {
if (pollIntervalRef.current) {
@@ -165,8 +185,21 @@ export default function WechatRegister({ subtitle }) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (warningTimeoutRef.current) {
clearTimeout(warningTimeoutRef.current);
warningTimeoutRef.current = null;
}
}, []);
// 重置所有状态(切换 Tab 时调用)
const resetState = useCallback(() => {
clearTimers();
setWechatAuthUrl("");
setWechatSessionId("");
setWechatStatus(WECHAT_STATUS.NONE);
sessionIdRef.current = null;
}, [clearTimers]);
const handleLoginSuccess = useCallback(async (sessionId, status) => {
try {
const response = await authService.loginWithWechat(sessionId);
@@ -237,6 +270,15 @@ export default function WechatRegister({ subtitle }) {
const startPolling = useCallback(() => {
clearTimers();
pollIntervalRef.current = setInterval(checkWechatStatus, POLL_INTERVAL);
// 2 分钟后显示即将过期提示
warningTimeoutRef.current = setTimeout(() => {
if (isMountedRef.current) {
message.warning("二维码即将过期,请尽快扫码", 5);
}
}, QR_CODE_WARNING_TIME);
// 5 分钟后过期
timeoutRef.current = setTimeout(() => {
clearTimers();
sessionIdRef.current = null;
@@ -245,9 +287,13 @@ export default function WechatRegister({ subtitle }) {
}, [checkWechatStatus, clearTimers]);
const getWechatQRCode = useCallback(async () => {
// 防止重复请求(使用 ref 避免闭包陷阱)
if (isLoadingRef.current) return;
try {
isLoadingRef.current = true;
setIsLoading(true);
const isRefresh = Boolean(wechatSessionId);
const isRefresh = Boolean(sessionIdRef.current); // 使用 ref 判断是否刷新
authEvents.trackWechatLoginInitiated(isRefresh ? 'qr_refresh' : 'qr_area');
const response = await authService.getWechatQRCode();
@@ -257,8 +303,9 @@ export default function WechatRegister({ subtitle }) {
throw new Error(response?.message || '获取二维码失败');
}
if (isRefresh) {
authEvents.trackWechatQRRefreshed(wechatSessionId, response.data.session_id);
const oldSessionId = sessionIdRef.current;
if (isRefresh && oldSessionId) {
authEvents.trackWechatQRRefreshed(oldSessionId, response.data.session_id);
} else {
authEvents.trackWechatQRDisplayed(response.data.session_id, response.data.auth_url);
}
@@ -267,15 +314,17 @@ export default function WechatRegister({ subtitle }) {
setWechatAuthUrl(response.data.auth_url);
setWechatSessionId(response.data.session_id);
setWechatStatus(WECHAT_STATUS.WAITING);
setIframeLoading(true); // 重置 iframe 加载状态
startPolling();
} catch (error) {
authEvents.trackError('api', error.message || '获取二维码失败');
logger.error('WechatRegister', 'getWechatQRCode', error);
message.error(error.message || "获取微信授权失败,请稍后重试");
} finally {
isLoadingRef.current = false;
if (isMountedRef.current) setIsLoading(false);
}
}, [startPolling, wechatSessionId, authEvents]);
}, [startPolling, authEvents]); // 移除 wechatSessionId 依赖,使用 ref
const handleGetQRCodeClick = useCallback(async () => {
try {
@@ -296,6 +345,12 @@ export default function WechatRegister({ subtitle }) {
};
}, [clearTimers]);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
fetchQRCode: getWechatQRCode,
stopPolling: resetState, // 停止轮询并重置状态
}), [getWechatQRCode, resetState]);
return (
<div style={styles.container}>
<Title level={5} style={styles.title}>微信登陆</Title>
@@ -305,22 +360,30 @@ export default function WechatRegister({ subtitle }) {
<div ref={containerRef} style={styles.qrcodeContainer}>
{wechatStatus === WECHAT_STATUS.WAITING ? (
<iframe
src={wechatAuthUrl}
title="微信扫码登录"
width="300"
height="350"
scrolling="no"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation"
style={{
border: 'none',
transform: 'scale(0.77) translateY(-35px)',
transformOrigin: 'top left',
marginLeft: '-5px',
pointerEvents: 'auto',
overflow: 'hidden',
}}
/>
<>
{iframeLoading && (
<div style={styles.iframeLoading}>
<span style={styles.loadingText}>二维码加载中...</span>
</div>
)}
<iframe
src={wechatAuthUrl}
title="微信扫码登录"
width="300"
height="350"
scrolling="no"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation"
onLoad={() => setIframeLoading(false)}
style={{
border: 'none',
transform: 'scale(0.77) translateY(-35px)',
transformOrigin: 'top left',
marginLeft: '-5px',
pointerEvents: 'auto',
overflow: 'hidden',
}}
/>
</>
) : (
<div style={styles.placeholder}>
<QrcodeOutlined style={styles.qrcodeIcon} />
@@ -374,4 +437,6 @@ export default function WechatRegister({ subtitle }) {
)}
</div>
);
}
});
export default WechatRegister;