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