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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user