From ddace54a9ddf8bd4275cc410b30e347d55c1671a Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 30 Dec 2025 15:59:58 +0800 Subject: [PATCH 1/2] =?UTF-8?q?style:=20=E7=BB=9F=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E4=B8=AD=E5=BF=83=E6=8C=89=E9=92=AE=E4=B8=8E=E6=8A=95?= =?UTF-8?q?=E8=B5=84=E6=97=A5=E5=8E=86=E6=8C=89=E9=92=AE=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MenuButton 改为 colorScheme="blue" + variant="solid" + borderRadius="full" - 移除未使用的 useColorModeValue 和 hoverBg 变量 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Navbars/components/Navigation/PersonalCenterMenu.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/Navbars/components/Navigation/PersonalCenterMenu.js b/src/components/Navbars/components/Navigation/PersonalCenterMenu.js index 8a84d97b..8817fe0a 100644 --- a/src/components/Navbars/components/Navigation/PersonalCenterMenu.js +++ b/src/components/Navbars/components/Navigation/PersonalCenterMenu.js @@ -12,7 +12,6 @@ import { Box, Text, Badge, - useColorModeValue, useDisclosure } from '@chakra-ui/react'; import { ChevronDown, Home, User, Settings, LogOut, Crown } from 'lucide-react'; @@ -28,7 +27,6 @@ import { useNavigate } from 'react-router-dom'; */ const PersonalCenterMenu = memo(({ user, handleLogout }) => { const navigate = useNavigate(); - const hoverBg = useColorModeValue('gray.100', 'gray.700'); // 🎯 为个人中心菜单创建 useDisclosure Hook const { isOpen, onOpen, onClose } = useDisclosure(); @@ -47,9 +45,10 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => { } - _hover={{ bg: hoverBg }} onMouseEnter={onOpen} onMouseLeave={onClose} > From 1e4924e34d591addd6d321d4a981810712e8440a Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 30 Dec 2025 16:46:04 +0800 Subject: [PATCH 2/2] =?UTF-8?q?style(Auth):=20=E7=99=BB=E5=BD=95=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E6=94=B9=E4=B8=BA=E9=BB=91=E9=87=91=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthModalManager: 使用 Ant Design Modal styles 属性设置黑金背景 - AuthFormContent: 表单组件使用内联样式实现黑金主题 - AuthHeader: 标题使用金色渐变效果 - VerificationCodeInput: 输入框和按钮黑金样式 - WechatRegister: 微信登录区域黑金样式 - AuthModal.less: 添加 placeholder 样式确保深色背景可见 主题色彩: - 主金色: #D4AF37 - 背景渐变: #1A1A2E → #16213e → #0F0F1A - 输入框背景: rgba(26, 26, 46, 0.8) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/Auth/AuthFormContent.js | 670 +++++++------------ src/components/Auth/AuthHeader.js | 59 +- src/components/Auth/AuthModal.less | 418 ++++++++++++ src/components/Auth/AuthModalManager.js | 115 +++- src/components/Auth/VerificationCodeInput.js | 121 ++-- src/components/Auth/WechatRegister.js | 619 ++++++----------- 6 files changed, 1065 insertions(+), 937 deletions(-) create mode 100644 src/components/Auth/AuthModal.less diff --git a/src/components/Auth/AuthFormContent.js b/src/components/Auth/AuthFormContent.js index 46f4e308..f166c4b4 100644 --- a/src/components/Auth/AuthFormContent.js +++ b/src/components/Auth/AuthFormContent.js @@ -1,34 +1,10 @@ // src/components/Auth/AuthFormContent.js -// 统一的认证表单组件 +// 统一的认证表单组件 - Ant Design 版本 - 黑金主题 import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; -import { - Box, - Button, - FormControl, - Input, - Heading, - VStack, - HStack, - Stack, - useToast, - Icon, - FormErrorMessage, - Center, - AlertDialog, - AlertDialogBody, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogContent, - AlertDialogOverlay, - Text, - Link as ChakraLink, - useBreakpointValue, - Divider, - IconButton, -} from "@chakra-ui/react"; -import { Lock } from "lucide-react"; -import { WechatOutlined } from '@ant-design/icons'; +import { Form, Input, Button, Typography, Space, Row, Col, Modal, message } from 'antd'; +import { LockOutlined, WechatOutlined } from '@ant-design/icons'; +import { useBreakpointValue } from "@chakra-ui/react"; import { useAuth } from "../../contexts/AuthContext"; import { useAuthModal } from "../../hooks/useAuthModal"; import { useNotification } from "../../contexts/NotificationContext"; @@ -36,93 +12,119 @@ import { authService } from "../../services/authService"; import AuthHeader from './AuthHeader'; import VerificationCodeInput from './VerificationCodeInput'; import WechatRegister from './WechatRegister'; -import { setCurrentUser } from '../../mocks/data/users'; import { logger } from '../../utils/logger'; import { getApiBase } from '../../utils/apiConfig'; import { useAuthEvents } from '../../hooks/useAuthEvents'; +const { Text, Link, Title } = Typography; + +// 黑金主题样式 +const THEME = { + goldPrimary: '#D4AF37', + goldLight: '#F4D03F', + goldDark: '#B8860B', + bgInput: 'rgba(26, 26, 46, 0.8)', + bgElevated: '#1A1A2E', + lineDefault: 'rgba(212, 175, 55, 0.3)', + lineEmphasis: 'rgba(212, 175, 55, 0.5)', + textPrimary: 'rgba(255, 255, 255, 0.95)', + textMuted: 'rgba(255, 255, 255, 0.4)', + wechat: '#07C160', +}; + +const styles = { + formTitle: { + color: THEME.goldPrimary, + fontWeight: 600, + letterSpacing: '0.05em', + marginBottom: '16px', + }, + input: { + background: THEME.bgInput, + border: `1px solid ${THEME.lineDefault}`, + color: THEME.textPrimary, + height: '44px', + borderRadius: '8px', + }, + submitBtn: { + height: '48px', + fontSize: '16px', + fontWeight: 'bold', + borderRadius: '8px', + background: `linear-gradient(135deg, ${THEME.goldPrimary} 0%, ${THEME.goldDark} 50%, ${THEME.goldPrimary} 100%)`, + border: `1px solid ${THEME.lineEmphasis}`, + color: THEME.bgElevated, + boxShadow: '0 4px 15px rgba(212, 175, 55, 0.3)', + }, + privacyText: { + textAlign: 'center', + fontSize: '12px', + color: THEME.textMuted, + marginTop: '12px', + }, + privacyLink: { + color: THEME.goldPrimary, + textDecoration: 'underline', + }, + wechatBtn: { + color: THEME.wechat, + padding: '4px 8px', + }, + otherLoginText: { + color: THEME.textMuted, + fontSize: '12px', + }, +}; + // 统一配置对象 const AUTH_CONFIG = { - // UI文本 title: "价值前沿", subtitle: "开启您的投资之旅", formTitle: "登陆/注册", buttonText: "登录/注册", loadingText: "验证中...", - successTitle: "验证成功", successDescription: "欢迎!", errorTitle: "验证失败", - - // API配置 api: { - endpoint: '/api/auth/register/phone', - purpose: 'login', // ⚡ 统一使用 'login' 模式 + purpose: 'login', }, - - // 功能开关 features: { - successDelay: 1000, // 延迟1秒显示成功提示 + successDelay: 1000, } }; export default function AuthFormContent() { - const toast = useToast(); const navigate = useNavigate(); const { checkSession } = useAuth(); const { handleLoginSuccess } = useAuthModal(); const { showWelcomeGuide } = useNotification(); - // 使用统一配置 const config = AUTH_CONFIG; - - // 追踪组件挂载状态,防止内存泄漏 const isMountedRef = useRef(true); - const cancelRef = useRef(); // AlertDialog 需要的 ref - // 页面状态 + // 状态 const [isLoading, setIsLoading] = useState(false); - const [errors, setErrors] = useState({}); - - // 昵称设置引导对话框 const [showNicknamePrompt, setShowNicknamePrompt] = useState(false); const [currentPhone, setCurrentPhone] = useState(""); - - // 响应式断点 - const isMobile = useBreakpointValue({ base: true, md: false }); - const stackDirection = useBreakpointValue({ base: "column", md: "row" }); - - // 事件追踪 - const authEvents = useAuthEvents({ - component: 'AuthFormContent', - isMobile, - }); - const stackSpacing = useBreakpointValue({ base: 4, md: 2 }); // ✅ 桌面端从32px减至8px,更紧凑 - - // 表单数据 - const [formData, setFormData] = useState({ - phone: "", - verificationCode: "", - }); - - // 验证码状态 + const [formData, setFormData] = useState({ phone: "", verificationCode: "" }); const [verificationCodeSent, setVerificationCodeSent] = useState(false); const [sendingCode, setSendingCode] = useState(false); const [countdown, setCountdown] = useState(0); - // 输入框变化处理 + // 响应式 + const isMobile = useBreakpointValue({ base: true, md: false }); + + // 事件追踪 + const authEvents = useAuthEvents({ component: 'AuthFormContent', isMobile }); + + // 输入变化处理 const handleInputChange = (e) => { const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value - })); + setFormData(prev => ({ ...prev, [name]: value })); - // 追踪用户开始填写手机号 (判断用户选择了手机登录方式) if (name === 'phone' && value.length === 1 && !formData.phone) { authEvents.trackPhoneLoginInitiated(value); } - - // 追踪验证码输入变化 if (name === 'verificationCode') { authEvents.trackVerificationCodeInputChanged(value.length); } @@ -131,470 +133,314 @@ export default function AuthFormContent() { // 倒计时逻辑 useEffect(() => { let timer; - let isMounted = true; - if (countdown > 0) { - timer = setInterval(() => { - if (isMounted) { - setCountdown(prev => prev - 1); - } - }, 1000); - } else if (countdown === 0 && isMounted) { + timer = setInterval(() => setCountdown(prev => prev - 1), 1000); + } else if (countdown === 0) { setVerificationCodeSent(false); } - - return () => { - isMounted = false; - if (timer) clearInterval(timer); - }; + return () => timer && clearInterval(timer); }, [countdown]); // 发送验证码 const sendVerificationCode = async () => { const credential = formData.phone; - if (!credential) { - toast({ - title: "请先输入手机号", - status: "warning", - duration: 3000, - }); + message.warning("请先输入手机号"); return; } - // 清理手机号格式字符(空格、横线、括号等) const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, ''); - if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) { authEvents.trackPhoneNumberValidated(credential, false, 'invalid_format'); - authEvents.trackFormValidationError('phone', 'invalid_format', '请输入有效的手机号'); - toast({ - title: "请输入有效的手机号", - status: "warning", - duration: 3000, - }); + message.warning("请输入有效的手机号"); return; } - // 追踪手机号验证通过 authEvents.trackPhoneNumberValidated(credential, true); try { setSendingCode(true); - - const requestData = { - credential: cleanedCredential, // 使用清理后的手机号 - type: 'phone', - purpose: config.api.purpose - }; - const response = await fetch(`${getApiBase()}/api/auth/send-verification-code`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify(requestData), + body: JSON.stringify({ + credential: cleanedCredential, + type: 'phone', + purpose: config.api.purpose + }), }); - if (!response) { - throw new Error('网络请求失败,请检查网络连接'); - } - const data = await response.json(); - if (!isMountedRef.current) return; - if (!data) { - throw new Error('服务器响应为空'); - } - if (response.ok && data.success) { - // 追踪验证码发送成功 (或重发) - const isResend = verificationCodeSent; - if (isResend) { + if (verificationCodeSent) { authEvents.trackVerificationCodeResent(credential, countdown > 0 ? 2 : 1); } else { authEvents.trackVerificationCodeSent(credential, config.api.purpose); } - - // ✅ 开发环境下在控制台显示验证码 if (data.dev_code) { console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;'); } - setVerificationCodeSent(true); setCountdown(60); } else { throw new Error(data.error || '发送验证码失败'); } } catch (error) { - // 追踪验证码发送失败 authEvents.trackVerificationCodeSendFailed(credential, error); - authEvents.trackError('api', error.message || '发送验证码失败', { - endpoint: '/api/auth/send-verification-code', - phone_masked: credential.substring(0, 3) + '****' + credential.substring(7) - }); - - logger.api.error('POST', '/api/auth/send-verification-code', error, { - credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7) - }); - - // ✅ 显示错误提示给用户 - toast({ - id: 'send-code-error', - title: "发送验证码失败", - description: error.message || "请稍后重试", - status: "error", - duration: 3000, - isClosable: true, - position: 'top', - containerStyle: { - zIndex: 10000, - } - }); + logger.api.error('POST', '/api/auth/send-verification-code', error); + message.error(error.message || "发送验证码失败"); } finally { - if (isMountedRef.current) { - setSendingCode(false); - } + if (isMountedRef.current) setSendingCode(false); } }; - // 提交处理(登录或注册) + // 提交表单 const handleSubmit = async (e) => { - e.preventDefault(); + e?.preventDefault?.(); setIsLoading(true); try { const { phone, verificationCode } = formData; - - // 表单验证 if (!phone || !verificationCode) { - toast({ - title: "请填写完整信息", - description: "手机号和验证码不能为空", - status: "warning", - duration: 3000, - }); + message.warning("手机号和验证码不能为空"); return; } - // 清理手机号格式字符(空格、横线、括号等) const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, ''); - if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) { - toast({ - title: "请输入有效的手机号", - status: "warning", - duration: 3000, - }); + message.warning("请输入有效的手机号"); return; } - // 追踪验证码提交 authEvents.trackVerificationCodeSubmitted(phone); - // 构建请求体 - const requestBody = { - credential: cleanedPhone, // 使用清理后的手机号 - verification_code: verificationCode.trim(), // 添加 trim() 防止空格 - login_type: 'phone', - }; - - // 调用API(根据模式选择不同的endpoint const response = await fetch(`${getApiBase()}/api/auth/login-with-code`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify(requestBody), + body: JSON.stringify({ + credential: cleanedPhone, + verification_code: verificationCode.trim(), + login_type: 'phone', + }), }); - if (!response) { - throw new Error('网络请求失败,请检查网络连接'); - } - const data = await response.json(); - if (!isMountedRef.current) return; - if (!data) { - throw new Error('服务器响应为空'); - } - if (response.ok && data.success) { - // 更新session await checkSession(); - - // ✅ 兼容后端两种命名格式:camelCase (isNewUser) 和 snake_case (is_new_user) const isNewUser = data.isNewUser ?? data.is_new_user ?? false; - - // 追踪登录成功并识别用户 authEvents.trackLoginSuccess(data.user, 'phone', isNewUser); + message.success(isNewUser ? '注册成功' : '登录成功'); - // ✅ 保留登录成功 toast(关键操作提示) - toast({ - title: isNewUser ? '注册成功' : '登录成功', - description: config.successDescription, - status: "success", - duration: 2000, - }); - - // 检查是否为新注册用户 if (isNewUser) { - // 新注册用户,延迟后显示昵称设置引导 setTimeout(() => { setCurrentPhone(phone); setShowNicknamePrompt(true); - // 追踪昵称设置引导显示 authEvents.trackNicknamePromptShown(phone); }, config.features.successDelay); } else { - // 已有用户,直接登录成功 - setTimeout(() => { - handleLoginSuccess({ phone }); - }, config.features.successDelay); + setTimeout(() => handleLoginSuccess({ phone }), config.features.successDelay); } - // ⚡ 延迟 10 秒显示权限引导(温和、非侵入) setTimeout(() => { - if (showWelcomeGuide) { - logger.info('AuthFormContent', '显示欢迎引导'); - showWelcomeGuide(); - } + if (showWelcomeGuide) showWelcomeGuide(); }, 10000); } else { - throw new Error(data.error || `${config.errorTitle}`); + throw new Error(data.error || config.errorTitle); } } catch (error) { const { phone, verificationCode } = formData; - - // 追踪登录失败 - const errorType = error.message.includes('网络') ? 'network' : - error.message.includes('服务器') ? 'api' : 'validation'; - authEvents.trackLoginFailed('phone', errorType, error.message, { + authEvents.trackLoginFailed('phone', 'api', error.message, { phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A', has_verification_code: !!verificationCode }); - - logger.error('AuthFormContent', 'handleSubmit', error, { - phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A', - hasVerificationCode: !!verificationCode - }); - - // ✅ 显示错误提示给用户 - toast({ - id: 'auth-verification-error', - title: config.errorTitle, - description: error.message || "请检查验证码是否正确", - status: "error", - duration: 3000, - isClosable: true, - position: 'top', - containerStyle: { - zIndex: 10000, - } - }); + logger.error('AuthFormContent', 'handleSubmit', error); + message.error(error.message || "验证失败"); } finally { - if (isMountedRef.current) { - setIsLoading(false); - } + if (isMountedRef.current) setIsLoading(false); } }; - // 微信H5登录处理 + // 微信H5登录 const handleWechatH5Login = async () => { - // 追踪用户选择微信登录 authEvents.trackWechatLoginInitiated('icon_button'); - try { - // 1. 构建回调URL,携带当前页面路径以便登录后返回 const currentPath = window.location.pathname + window.location.search; const returnUrl = encodeURIComponent(currentPath); const redirectUrl = `${window.location.origin}/home/wechat-callback?returnUrl=${returnUrl}`; - // 2. 显示提示 - toast({ - title: "即将跳转", - description: "正在跳转到微信授权页面...", - status: "info", - duration: 2000, - isClosable: true, - }); - - // 3. 获取微信H5授权URL + message.info("正在跳转到微信授权页面..."); const response = await authService.getWechatH5AuthUrl(redirectUrl); - if (!response || !response.auth_url) { - throw new Error('获取授权链接失败'); - } + if (!response?.auth_url) throw new Error('获取授权链接失败'); - // 追踪微信H5跳转 authEvents.trackWechatH5Redirect(); - - // 4. 延迟跳转,让用户看到提示 - setTimeout(() => { - window.location.href = response.auth_url; - }, 500); + setTimeout(() => { window.location.href = response.auth_url; }, 500); } catch (error) { - // 追踪跳转失败 - authEvents.trackError('api', error.message || '获取微信授权链接失败', { - context: 'wechat_h5_redirect' - }); - + authEvents.trackError('api', error.message || '获取微信授权链接失败'); logger.error('AuthFormContent', 'handleWechatH5Login', error); - toast({ - title: "跳转失败", - description: error.message || "请稍后重试", - status: "error", - duration: 3000, - isClosable: true, - }); + message.error(error.message || "跳转失败"); } }; - // 组件挂载时追踪页面浏览 useEffect(() => { isMountedRef.current = true; - - // 追踪登录页面浏览 authEvents.trackLoginPageViewed(); - - return () => { - isMountedRef.current = false; - }; + return () => { isMountedRef.current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // 仅在挂载时执行一次,避免 countdown 倒计时导致重复触发 + }, []); return ( - <> - - - - -
- - {config.formTitle} - - - {errors.phone} - +
+ - {/* 验证码输入框 + 移动端微信图标 */} - - + + {/* 左侧表单 */} + + {config.formTitle} - {/* 移动端:验证码下方的微信登录图标 */} - {isMobile && ( - - 其他登录方式: - } - size="sm" - variant="ghost" - color="#07C160" - borderRadius="md" - minW="24px" - minH="24px" - _hover={{ - bg: "green.50", - color: "#06AD56" - }} - _active={{ - bg: "green.100" - }} - onClick={handleWechatH5Login} - isDisabled={isLoading} - /> - - )} - + + + + - + + + - {/* 隐私声明 */} - - 登录即表示您同意价值前沿{" "} - - 《用户协议》 - - {" "}和{" "} - - 《隐私政策》 - - - - - - - {/* 桌面端:右侧二维码扫描 */} - {!isMobile && ( - {/* ✅ 桌面端让右侧自适应宽度 */} -
{/* ✅ 移除bg和p,WechatRegister自带白色背景和padding */} - -
-
- )} - - - - {/* 只在需要时才渲染 AlertDialog,避免创建不必要的 Portal */} - {showNicknamePrompt && ( - { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }} isCentered closeOnEsc={true} closeOnOverlayClick={false}> - - - 完善个人信息 - 您已成功注册!是否前往个人资料设置昵称和其他信息? - + {/* 移动端微信登录 */} + {isMobile && ( +
+ 其他登录方式: - - - - - - )} - + type="text" + icon={} + onClick={handleWechatH5Login} + disabled={isLoading} + style={styles.wechatBtn} + /> +
+ )} + + + + + +
+ 登录即表示您同意价值前沿{" "} + + 《用户协议》 + + {" "}和{" "} + + 《隐私政策》 + +
+ + + + {/* 右侧微信二维码 - 仅桌面端 */} + {!isMobile && ( + + + + )} + + + {/* 昵称设置引导 */} + { + authEvents.trackNicknamePromptSkipped(); + setShowNicknamePrompt(false); + handleLoginSuccess({ phone: currentPhone }); + }} + footer={[ + , + + ]} + styles={{ + content: { + background: THEME.bgElevated, + border: `1px solid ${THEME.lineDefault}`, + }, + header: { + background: 'transparent', + borderBottom: `1px solid ${THEME.lineDefault}`, + }, + body: { + color: THEME.textPrimary, + }, + footer: { + borderTop: `1px solid ${THEME.lineDefault}`, + }, + }} + > + + 您已成功注册!是否前往个人资料设置昵称和其他信息? + + +
); } diff --git a/src/components/Auth/AuthHeader.js b/src/components/Auth/AuthHeader.js index ba256868..d6685fb1 100644 --- a/src/components/Auth/AuthHeader.js +++ b/src/components/Auth/AuthHeader.js @@ -1,23 +1,46 @@ // src/components/Auth/AuthHeader.js +// 认证页面头部组件 - 黑金主题 import React from "react"; -import { Heading, Text, VStack } from "@chakra-ui/react"; +import { Typography } from "antd"; + +const { Title, Text } = Typography; + +// 黑金主题样式 +const styles = { + header: { + textAlign: 'center', + marginBottom: '24px', + }, + title: { + marginBottom: '12px', + fontWeight: 'bold', + fontSize: '28px', + background: 'linear-gradient(to right, #D4AF37, #F4D03F, #D4AF37)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + backgroundClip: 'text', + letterSpacing: '0.1em', + }, + divider: { + width: '60px', + height: '2px', + margin: '0 auto 12px', + background: 'linear-gradient(to right, transparent, #D4AF37, transparent)', + borderRadius: '9999px', + }, + subtitle: { + color: 'rgba(212, 175, 55, 0.8)', + fontSize: '14px', + letterSpacing: '0.05em', + }, +}; -/** - * 认证页面通用头部组件 - * 用于显示页面标题和描述 - * - * @param {string} title - 主标题文字 - * @param {string} subtitle - 副标题文字 - */ export default function AuthHeader({ title, subtitle }) { - return ( - - - {title} - - - {subtitle} - - - ); + return ( +
+ {title} +
+ {subtitle} +
+ ); } diff --git a/src/components/Auth/AuthModal.less b/src/components/Auth/AuthModal.less new file mode 100644 index 00000000..c482ae95 --- /dev/null +++ b/src/components/Auth/AuthModal.less @@ -0,0 +1,418 @@ +/* AuthModal.less - 登录弹窗黑金主题样式 */ + +// ==================== 变量定义 ==================== +@color-bg-deep: #0A0A14; +@color-bg-primary: #0F0F1A; +@color-bg-elevated: #1A1A2E; +@color-bg-surface: #16213e; +@color-bg-input: rgba(26, 26, 46, 0.8); + +@color-gold-primary: #D4AF37; +@color-gold-light: #F4D03F; +@color-gold-dark: #B8860B; +@color-gold-darker: #8B6914; +@color-gold-gradient: linear-gradient(135deg, @color-gold-primary 0%, @color-gold-dark 50%, @color-gold-primary 100%); + +@color-line-subtle: rgba(212, 175, 55, 0.1); +@color-line-default: rgba(212, 175, 55, 0.3); +@color-line-emphasis: rgba(212, 175, 55, 0.5); + +@color-text-primary: rgba(255, 255, 255, 0.95); +@color-text-secondary: rgba(255, 255, 255, 0.6); +@color-text-muted: rgba(255, 255, 255, 0.4); + +@color-wechat: #07C160; + +// ==================== Modal 遮罩层 ==================== +.auth-modal-root { + &.ant-modal-root .ant-modal-mask { + background: rgba(0, 0, 0, 0.85) !important; + backdrop-filter: blur(4px); + } +} + +// ==================== Modal 主体样式 ==================== +.auth-modal.ant-modal { + .ant-modal-content { + background: linear-gradient(135deg, @color-bg-elevated 0%, @color-bg-surface 50%, @color-bg-primary 100%) !important; + border: 1px solid @color-line-default !important; + border-radius: 16px !important; + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.5), + 0 0 60px rgba(212, 175, 55, 0.15), + inset 0 1px 0 rgba(212, 175, 55, 0.1) !important; + overflow: hidden; + } + + .ant-modal-header { + display: none !important; + } + + .ant-modal-body { + padding: 32px !important; + max-height: calc(90vh - 120px); + overflow-y: auto; + background: transparent !important; + } + + .ant-modal-close { + color: @color-text-secondary !important; + transition: all 0.3s ease; + + &:hover { + color: @color-gold-primary !important; + background: rgba(212, 175, 55, 0.1) !important; + } + } +} + +// ==================== 表单内容区域 ==================== +.auth-form-content { + width: 100%; +} + +// ==================== 头部样式 ==================== +.auth-modal .auth-header { + text-align: center; + margin-bottom: 24px; + + .auth-title.ant-typography { + margin-bottom: 12px !important; + font-weight: bold !important; + font-size: 28px !important; + background: linear-gradient(to right, @color-gold-primary, @color-gold-light, @color-gold-primary) !important; + -webkit-background-clip: text !important; + -webkit-text-fill-color: transparent !important; + background-clip: text !important; + letter-spacing: 0.1em; + color: transparent !important; + } + + .auth-divider { + width: 60px; + height: 2px; + margin: 0 auto 12px; + background: linear-gradient(to right, transparent, @color-gold-primary, transparent); + border-radius: 9999px; + } + + .auth-subtitle.ant-typography { + color: rgba(212, 175, 55, 0.8) !important; + font-size: 14px; + letter-spacing: 0.05em; + } +} + +// ==================== 表单标题 ==================== +.auth-modal .form-section-title.ant-typography { + color: @color-gold-primary !important; + font-weight: 600 !important; + letter-spacing: 0.05em; + margin-bottom: 16px !important; +} + +// ==================== 输入框样式 ==================== +// 全局 Ant Design Input placeholder 样式(黑金主题) +// placeholder 使用更亮的颜色以便在深色背景上清晰可见 +@color-placeholder: rgba(255, 255, 255, 0.5); + +.auth-modal .ant-input, +.auth-modal-root .ant-input { + &::placeholder { + color: @color-placeholder !important; + opacity: 1 !important; + } + + &::-webkit-input-placeholder { + color: @color-placeholder !important; + opacity: 1 !important; + } + + &::-moz-placeholder { + color: @color-placeholder !important; + opacity: 1 !important; + } + + &:-ms-input-placeholder { + color: @color-placeholder !important; + opacity: 1 !important; + } +} + +// ==================== 验证码按钮 ==================== +.auth-modal .send-code-btn.ant-btn { + background: transparent !important; + color: @color-gold-primary !important; + border: 1px solid @color-line-emphasis !important; + border-left: none !important; + min-width: 120px; + height: 44px; + border-radius: 0 8px 8px 0 !important; + + &:hover:not(:disabled) { + background: rgba(212, 175, 55, 0.1) !important; + border-color: @color-gold-primary !important; + color: @color-gold-primary !important; + } + + &:disabled, + &.ant-btn-disabled { + opacity: 0.5; + color: @color-text-muted !important; + border-color: @color-line-default !important; + background: transparent !important; + } +} + +// ==================== 提交按钮 ==================== +.auth-modal .submit-btn.ant-btn { + height: 48px !important; + font-size: 16px !important; + font-weight: bold !important; + border-radius: 8px !important; + background: @color-gold-gradient !important; + border: 1px solid @color-line-emphasis !important; + color: @color-bg-elevated !important; + box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3); + transition: all 0.2s ease; + + &:hover:not(:disabled) { + background: linear-gradient(135deg, @color-gold-light 0%, @color-gold-primary 50%, @color-gold-light 100%) !important; + box-shadow: 0 6px 20px rgba(212, 175, 55, 0.4); + transform: translateY(-1px); + color: @color-bg-elevated !important; + border-color: @color-line-emphasis !important; + } + + &:active:not(:disabled) { + background: linear-gradient(135deg, @color-gold-dark 0%, @color-gold-darker 50%, @color-gold-dark 100%) !important; + transform: translateY(0); + } + + // Ant Design primary 按钮覆盖 + &.ant-btn-primary { + background: @color-gold-gradient !important; + border-color: @color-line-emphasis !important; + color: @color-bg-elevated !important; + } +} + +// ==================== 隐私声明 ==================== +.auth-modal .privacy-text { + text-align: center; + font-size: 12px; + color: @color-text-muted; + margin-top: 12px; + + a, + .ant-typography-link { + color: @color-gold-primary !important; + text-decoration: underline; + + &:hover { + color: @color-gold-light !important; + } + } +} + +// ==================== 移动端微信登录 ==================== +.auth-modal .mobile-wechat-login { + display: flex; + align-items: center; + margin-bottom: 16px; + + .other-login-text.ant-typography { + color: @color-text-muted !important; + font-size: 12px; + } + + .wechat-btn.ant-btn { + color: @color-wechat !important; + padding: 4px 8px; + + &:hover { + background: rgba(7, 193, 96, 0.15) !important; + } + } +} + +// ==================== 微信登录区域 ==================== +.auth-modal .wechat-register { + text-align: center; + + .wechat-title.ant-typography { + color: @color-gold-primary !important; + font-weight: 600 !important; + letter-spacing: 0.05em; + margin-bottom: 12px !important; + } +} + +.auth-modal .qrcode-container { + position: relative; + width: 230px; + height: 230px; + margin: 0 auto; + overflow: hidden; + border-radius: 8px; + border: 1px solid @color-line-default; + background: rgba(255, 255, 255, 0.95); + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.3), + 0 0 30px rgba(212, 175, 55, 0.1); +} + +.auth-modal .qrcode-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background: rgba(26, 26, 46, 0.95); + + .qrcode-icon { + font-size: 64px; + color: rgba(212, 175, 55, 0.4); + margin-bottom: 16px; + } + + .get-qrcode-btn.ant-btn { + background: @color-gold-gradient !important; + border: 1px solid @color-line-emphasis !important; + color: @color-bg-elevated !important; + font-weight: 600; + + &:hover { + background: linear-gradient(135deg, @color-gold-light 0%, @color-gold-primary 50%, @color-gold-light 100%) !important; + box-shadow: 0 4px 15px rgba(212, 175, 55, 0.4); + } + } +} + +.auth-modal .expired-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(26, 26, 46, 0.9); + backdrop-filter: blur(4px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + + .expired-icon { + font-size: 32px; + color: @color-gold-primary; + } + + .expired-text.ant-typography { + color: @color-text-secondary !important; + font-size: 14px; + } + + .refresh-btn.ant-btn { + background: transparent !important; + color: @color-gold-primary !important; + border: 1px solid @color-line-emphasis !important; + + &:hover { + background: rgba(212, 175, 55, 0.1) !important; + border-color: @color-gold-primary !important; + } + } +} + +.auth-modal .status-text.ant-typography { + display: block; + text-align: center; + margin-top: 12px; + font-size: 14px; + font-weight: 500; +} + +// ==================== Mock 模式 ==================== +.auth-modal .mock-section { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid @color-line-subtle; + + .mock-btn.ant-btn { + background: transparent !important; + color: #9370DB !important; + border: 1px solid rgba(147, 112, 219, 0.5) !important; + + &:hover { + background: rgba(147, 112, 219, 0.1) !important; + border-color: #9370DB !important; + } + } + + .mock-hint.ant-typography { + display: block; + text-align: center; + color: @color-text-muted !important; + font-size: 12px; + margin-top: 4px; + } +} + +// ==================== 昵称弹窗 ==================== +.nickname-modal.ant-modal { + .ant-modal-content { + background: @color-bg-elevated !important; + border: 1px solid @color-line-default !important; + } + + .ant-modal-header { + background: transparent !important; + border-bottom: 1px solid @color-line-subtle !important; + + .ant-modal-title { + color: @color-gold-primary !important; + } + } + + .ant-modal-body { + color: @color-text-primary !important; + } + + .ant-modal-footer { + border-top: 1px solid @color-line-subtle !important; + + .ant-btn-primary { + background: @color-gold-gradient !important; + border-color: @color-line-emphasis !important; + color: @color-bg-elevated !important; + + &:hover { + background: linear-gradient(135deg, @color-gold-light 0%, @color-gold-primary 50%, @color-gold-light 100%) !important; + } + } + } +} + +// ==================== 响应式适配 ==================== +@media (max-width: 768px) { + .auth-modal.ant-modal { + .ant-modal-content { + border-radius: 12px !important; + } + + .ant-modal-body { + padding: 24px 16px !important; + } + } + + .auth-modal .auth-header .auth-title.ant-typography { + font-size: 24px !important; + } + + .auth-modal .qrcode-container { + width: 200px; + height: 200px; + } +} diff --git a/src/components/Auth/AuthModalManager.js b/src/components/Auth/AuthModalManager.js index 17193276..d4a29051 100644 --- a/src/components/Auth/AuthModalManager.js +++ b/src/components/Auth/AuthModalManager.js @@ -1,16 +1,42 @@ // src/components/Auth/AuthModalManager.js -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { Modal } from 'antd'; import { useBreakpointValue } from '@chakra-ui/react'; import { useAuthModal } from '../../hooks/useAuthModal'; import AuthFormContent from './AuthFormContent'; import { trackEventAsync } from '@lib/posthog'; import { ACTIVATION_EVENTS } from '@lib/constants'; -import { GLASS_BLUR } from '@/constants/glassConfig'; +import './AuthModal.less'; + +// 黑金主题色彩常量 +const THEME = { + bgElevated: '#1A1A2E', + bgSurface: '#16213e', + bgPrimary: '#0F0F1A', + goldPrimary: '#D4AF37', + lineDefault: 'rgba(212, 175, 55, 0.3)', + textSecondary: 'rgba(255, 255, 255, 0.6)', +}; + +// 全局样式注入 - 确保 placeholder 在深色背景上可见 +const GlobalInputStyles = () => ( + +); /** * 全局认证弹窗管理器 - * 统一的登录/注册弹窗 + * 统一的登录/注册弹窗 - 黑金主题 */ export default function AuthModalManager() { const { @@ -27,7 +53,7 @@ export default function AuthModalManager() { trackEventAsync(ACTIVATION_EVENTS.LOGIN_PAGE_VIEWED, { timestamp: new Date().toISOString(), modal_type: 'auth_modal', - trigger_source: 'user_action', // 可以通过 props 传递更精确的来源 + trigger_source: 'user_action', }); hasTrackedOpen.current = true; @@ -39,43 +65,64 @@ export default function AuthModalManager() { } }, [isAuthModalOpen]); - // 响应式宽度配置(Ant Design Modal 使用数字或字符串) + // 响应式宽度配置 const modalMaxW = useBreakpointValue( { - base: "90%", // 移动端:屏幕宽度的90% - sm: "90%", // 小屏:90% - md: "700px", // 中屏:固定700px - lg: "700px" // 大屏:固定700px + base: "90%", + sm: "90%", + md: "700px", + lg: "700px" }, { fallback: "700px", ssr: false } ); - // ✅ 使用 Ant Design Modal,完全避开 Chakra UI Portal 的 AnimatePresence 问题 - // Ant Design Modal 不使用 Framer Motion,不会有 React 18 并发渲染的 insertBefore 错误 + // Ant Design 5.x Modal styles 属性 - 直接覆盖 CSS-in-JS 样式 + const modalStyles = useMemo(() => ({ + mask: { + background: 'rgba(0, 0, 0, 0.85)', + backdropFilter: 'blur(4px)', + }, + content: { + background: `linear-gradient(135deg, ${THEME.bgElevated} 0%, ${THEME.bgSurface} 50%, ${THEME.bgPrimary} 100%)`, + border: `1px solid ${THEME.lineDefault}`, + borderRadius: '16px', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.5), 0 0 60px rgba(212, 175, 55, 0.15), inset 0 1px 0 rgba(212, 175, 55, 0.1)', + overflow: 'hidden', + padding: 0, + }, + header: { + display: 'none', + }, + body: { + padding: '32px', + maxHeight: 'calc(90vh - 120px)', + overflowY: 'auto', + background: 'transparent', + }, + }), []); + return ( - + + ✕ } - }} - > - - + > + + + ); } diff --git a/src/components/Auth/VerificationCodeInput.js b/src/components/Auth/VerificationCodeInput.js index 288b3312..8ad76696 100644 --- a/src/components/Auth/VerificationCodeInput.js +++ b/src/components/Auth/VerificationCodeInput.js @@ -1,74 +1,99 @@ -import React from "react"; -import { FormControl, FormErrorMessage, HStack, Input, Button, Spinner } from "@chakra-ui/react"; +// src/components/Auth/VerificationCodeInput.js +// 验证码输入组件 - 黑金主题 +import React from 'react'; +import { Input, Button, Space } from 'antd'; +import { LoadingOutlined } from '@ant-design/icons'; import { logger } from "../../utils/logger"; -/** - * 通用验证码输入组件 - */ +// 黑金主题样式 +const THEME = { + goldPrimary: '#D4AF37', + bgInput: 'rgba(26, 26, 46, 0.8)', + lineDefault: 'rgba(212, 175, 55, 0.3)', + lineEmphasis: 'rgba(212, 175, 55, 0.5)', + textPrimary: 'rgba(255, 255, 255, 0.95)', + textMuted: 'rgba(255, 255, 255, 0.4)', +}; + +const styles = { + input: { + background: THEME.bgInput, + border: `1px solid ${THEME.lineDefault}`, + color: THEME.textPrimary, + height: '44px', + borderRadius: '8px 0 0 8px', + flex: 1, + }, + sendCodeBtn: { + background: 'transparent', + color: THEME.goldPrimary, + border: `1px solid ${THEME.lineEmphasis}`, + borderLeft: 'none', + minWidth: '120px', + height: '44px', + borderRadius: '0 8px 8px 0', + }, + sendCodeBtnDisabled: { + background: 'transparent', + color: THEME.textMuted, + border: `1px solid ${THEME.lineDefault}`, + borderLeft: 'none', + minWidth: '120px', + height: '44px', + borderRadius: '0 8px 8px 0', + opacity: 0.5, + }, +}; + export default function VerificationCodeInput({ value, onChange, onSendCode, - countdown, - isLoading, - isSending, - error, + countdown = 0, + isLoading = false, + isSending = false, placeholder = "请输入6位验证码", buttonText = "获取验证码", countdownText = (count) => `${count}s`, - colorScheme = "green", - isRequired = true }) { - // 包装 onSendCode,确保所有错误都被捕获,防止被 ErrorBoundary 捕获 const handleSendCode = async () => { try { if (onSendCode) { await onSendCode(); } } catch (error) { - // 错误已经在父组件处理,这里只需要防止未捕获的 Promise rejection - logger.error('VerificationCodeInput', 'handleSendCode', error, { - hasOnSendCode: !!onSendCode, - countdown, - isLoading, - isSending - }); + logger.error('VerificationCodeInput', 'handleSendCode', error); } }; - // 计算按钮显示的文本(避免在 JSX 中使用条件渲染) const getButtonText = () => { - if (isSending) { - return "发送中"; - } - if (countdown > 0) { - return countdownText(countdown); - } + if (isSending) return "发送中..."; + if (countdown > 0) return countdownText(countdown); return buttonText; }; + const isDisabled = countdown > 0 || isLoading || isSending; + return ( - - - - - - {error} - + + + + ); } diff --git a/src/components/Auth/WechatRegister.js b/src/components/Auth/WechatRegister.js index bf064b61..57a4db6a 100644 --- a/src/components/Auth/WechatRegister.js +++ b/src/components/Auth/WechatRegister.js @@ -1,208 +1,206 @@ -import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from "react"; -import { - Box, - Button, - VStack, - HStack, - Center, - Text, - Heading, - Icon, - useToast, - Spinner -} from "@chakra-ui/react"; -import { QrCode, AlertCircle } from "lucide-react"; -import { useNavigate } from "react-router-dom"; +// src/components/Auth/WechatRegister.js +// 微信登录组件 - 黑金主题 +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { Typography, Button, message } from 'antd'; +import { QrcodeOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService"; import { useAuthModal } from "../../hooks/useAuthModal"; import { useAuth } from "../../contexts/AuthContext"; import { logger } from "../../utils/logger"; import { useAuthEvents } from "../../hooks/useAuthEvents"; -import { GLASS_BLUR } from "@/constants/glassConfig"; + +const { Title, Text } = Typography; // 配置常量 -const POLL_INTERVAL = 2000; // 轮询间隔:2秒 -const BACKUP_POLL_INTERVAL = 3000; // 备用轮询间隔:3秒 -const QR_CODE_TIMEOUT = 300000; // 二维码超时:5分钟 +const POLL_INTERVAL = 2000; +const QR_CODE_TIMEOUT = 300000; + +// 黑金主题样式 +const THEME = { + goldPrimary: '#D4AF37', + goldLight: '#F4D03F', + goldDark: '#B8860B', + bgInput: 'rgba(26, 26, 46, 0.8)', + bgElevated: '#1A1A2E', + lineDefault: 'rgba(212, 175, 55, 0.3)', + lineEmphasis: 'rgba(212, 175, 55, 0.5)', + textSecondary: 'rgba(255, 255, 255, 0.6)', +}; + +const styles = { + container: { + textAlign: 'center', + }, + title: { + color: THEME.goldPrimary, + fontWeight: 600, + letterSpacing: '0.05em', + marginBottom: '12px', + }, + qrcodeContainer: { + position: 'relative', + width: '230px', + height: '230px', + margin: '0 auto', + overflow: 'hidden', + borderRadius: '8px', + border: `1px solid ${THEME.lineDefault}`, + background: 'rgba(255, 255, 255, 0.95)', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3), 0 0 30px rgba(212, 175, 55, 0.1)', + }, + placeholder: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '100%', + background: 'rgba(26, 26, 46, 0.95)', + }, + qrcodeIcon: { + fontSize: '64px', + color: 'rgba(212, 175, 55, 0.4)', + marginBottom: '16px', + }, + getQrcodeBtn: { + background: `linear-gradient(135deg, ${THEME.goldPrimary} 0%, ${THEME.goldDark} 50%, ${THEME.goldPrimary} 100%)`, + border: `1px solid ${THEME.lineEmphasis}`, + color: THEME.bgElevated, + fontWeight: 600, + }, + expiredOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'rgba(26, 26, 46, 0.9)', + backdropFilter: 'blur(4px)', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '8px', + }, + expiredIcon: { + fontSize: '32px', + color: THEME.goldPrimary, + }, + expiredText: { + color: THEME.textSecondary, + fontSize: '14px', + }, + refreshBtn: { + background: 'transparent', + color: THEME.goldPrimary, + border: `1px solid ${THEME.lineEmphasis}`, + }, + statusText: { + display: 'block', + textAlign: 'center', + marginTop: '12px', + fontSize: '14px', + fontWeight: 500, + }, + mockSection: { + marginTop: '12px', + paddingTop: '12px', + borderTop: '1px solid rgba(212, 175, 55, 0.1)', + }, + mockBtn: { + background: 'transparent', + color: '#9370DB', + border: '1px solid rgba(147, 112, 219, 0.5)', + }, + mockHint: { + display: 'block', + textAlign: 'center', + color: 'rgba(255, 255, 255, 0.4)', + fontSize: '12px', + marginTop: '4px', + }, +}; -/** - * 获取状态文字颜色 - */ const getStatusColor = (status) => { switch(status) { - case WECHAT_STATUS.WAITING: return "gray.600"; // ✅ 灰色文字 - case WECHAT_STATUS.SCANNED: return "green.600"; // ✅ 绿色文字 - case WECHAT_STATUS.AUTHORIZED: return "green.600"; // ✅ 绿色文字 - case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字 - case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字 - case WECHAT_STATUS.REGISTER_SUCCESS: return "green.600"; - case WECHAT_STATUS.AUTH_DENIED: return "red.600"; // ✅ 红色文字 - case WECHAT_STATUS.AUTH_FAILED: return "red.600"; // ✅ 红色文字 - default: return "gray.600"; + case WECHAT_STATUS.WAITING: return "rgba(255, 255, 255, 0.6)"; + case WECHAT_STATUS.SCANNED: + case WECHAT_STATUS.AUTHORIZED: + case WECHAT_STATUS.LOGIN_SUCCESS: + case WECHAT_STATUS.REGISTER_SUCCESS: + return "#D4AF37"; + case WECHAT_STATUS.EXPIRED: return "#CD853F"; + case WECHAT_STATUS.AUTH_DENIED: + case WECHAT_STATUS.AUTH_FAILED: + return "#CD5C5C"; + default: return "rgba(255, 255, 255, 0.6)"; } }; -/** - * 获取状态文字 - */ -const getStatusText = (status) => { - return STATUS_MESSAGES[status] || "点击按钮获取二维码"; -}; +const getStatusText = (status) => STATUS_MESSAGES[status] || "点击按钮获取二维码"; export default function WechatRegister() { - // 获取关闭弹窗方法 const { closeModal } = useAuthModal(); const { refreshSession } = useAuth(); + const authEvents = useAuthEvents({ component: 'WechatRegister', isMobile: false }); - // 事件追踪 - const authEvents = useAuthEvents({ - component: 'WechatRegister', - isMobile: false // 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 backupPollIntervalRef = useRef(null); // 备用轮询定时器 const timeoutRef = useRef(null); - const isMountedRef = useRef(true); // 追踪组件挂载状态 - const containerRef = useRef(null); // 容器DOM引用 - const sessionIdRef = useRef(null); // 存储最新的 sessionId,避免闭包陷阱 - const wechatStatusRef = useRef(WECHAT_STATUS.NONE); // 存储最新的 wechatStatus,避免闭包陷阱 + const isMountedRef = useRef(true); + const containerRef = useRef(null); + const sessionIdRef = useRef(null); + const wechatStatusRef = useRef(WECHAT_STATUS.NONE); - 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]); - - /** - * 清理所有定时器 - * 注意:不清理 sessionIdRef,因为 startPolling 时也会调用此函数 - */ const clearTimers = useCallback(() => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } - if (backupPollIntervalRef.current) { - clearInterval(backupPollIntervalRef.current); - backupPollIntervalRef.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) { - // 追踪微信登录成功 - authEvents.trackLoginSuccess( - response.user, - 'wechat', - response.isNewUser || false - ); - - // 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 ? "登录成功" : "欢迎回来!" - ); - - // 刷新 AuthContext 状态 + authEvents.trackLoginSuccess(response.user, 'wechat', response.isNewUser || false); + if (response.token) localStorage.setItem('token', response.token); + if (response.user) localStorage.setItem('user', JSON.stringify(response.user)); + message.success(status === WECHAT_STATUS.LOGIN_SUCCESS ? "登录成功" : "欢迎回来!"); await refreshSession(); - - // 关闭认证弹窗,留在当前页面 closeModal(); } else { throw new Error(response?.error || '登录失败'); } } catch (error) { - // 追踪微信登录失败 - authEvents.trackLoginFailed('wechat', 'api', error.message || '登录失败', { - session_id: sessionId?.substring(0, 8) + '...', - status: status - }); - - logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId }); - showError("登录失败", error.message || "请重试"); + authEvents.trackLoginFailed('wechat', 'api', error.message || '登录失败'); + logger.error('WechatRegister', 'handleLoginSuccess', error); + message.error(error.message || "登录失败,请重试"); } - }, [showSuccess, showError, closeModal, refreshSession, authEvents]); + }, [closeModal, refreshSession, authEvents]); - /** - * 检查微信扫码状态 - * 使用 sessionIdRef.current 避免闭包陷阱 - */ const checkWechatStatus = useCallback(async () => { - // 检查组件是否已卸载,使用 ref 获取最新的 sessionId - if (!isMountedRef.current || !sessionIdRef.current) { - return; - } - + if (!isMountedRef.current || !sessionIdRef.current) return; const currentSessionId = sessionIdRef.current; try { const response = await authService.checkWechatStatus(currentSessionId); - - // 安全检查:确保 response 存在且包含 status - if (!response || typeof response.status === 'undefined') { - return; - } + if (!response || typeof response.status === 'undefined') return; const { status } = response; - - // 组件卸载后不再更新状态 if (!isMountedRef.current) return; - // 追踪状态变化(使用 ref 获取最新状态,避免闭包陷阱) const previousStatus = wechatStatusRef.current; if (previousStatus !== status) { authEvents.trackWechatStatusChanged(currentSessionId, previousStatus, status); - - // 特别追踪扫码事件 if (status === WECHAT_STATUS.SCANNED) { authEvents.trackWechatQRScanned(currentSessionId); } @@ -210,282 +208,100 @@ export default function WechatRegister() { setWechatStatus(status); - // 处理成功状态 if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) { - clearTimers(); // 停止轮询 - sessionIdRef.current = null; // 清理 sessionId - + clearTimers(); + sessionIdRef.current = null; await handleLoginSuccess(currentSessionId, status); - } - // 处理过期状态 - else if (status === WECHAT_STATUS.EXPIRED) { - // 追踪二维码过期 + } else if (status === WECHAT_STATUS.EXPIRED) { authEvents.trackWechatQRExpired(currentSessionId, QR_CODE_TIMEOUT / 1000); - clearTimers(); - sessionIdRef.current = null; // 清理 sessionId - if (isMountedRef.current) { - toast({ - title: "授权已过期", - description: "请重新获取授权", - status: "warning", - duration: 3000, - isClosable: true, - }); - } - } - // 处理用户拒绝授权 - else if (status === WECHAT_STATUS.AUTH_DENIED) { + sessionIdRef.current = null; + message.warning("授权已过期,请重新获取"); + } else if (status === WECHAT_STATUS.AUTH_DENIED) { clearTimers(); - if (isMountedRef.current) { - toast({ - title: "授权已取消", - description: "您已取消微信授权登录", - status: "warning", - duration: 3000, - isClosable: true, - }); - } - } - // 处理授权失败 - else if (status === WECHAT_STATUS.AUTH_FAILED) { + message.warning("您已取消微信授权登录"); + } else if (status === WECHAT_STATUS.AUTH_FAILED) { clearTimers(); - if (isMountedRef.current) { - const errorMsg = response.error || "授权过程出现错误"; - toast({ - title: "授权失败", - description: errorMsg, - status: "error", - duration: 5000, - isClosable: true, - }); - } + message.error(response.error || "授权过程出现错误"); } - // AUTHORIZED 状态只是中间状态,继续轮询等待 login_ready/register_ready - // 不在此处调用登录接口,避免"会话状态无效"错误 } catch (error) { - logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId }); - // 轮询过程中的错误不显示给用户,避免频繁提示 - // 但如果错误持续发生,停止轮询避免无限重试 + logger.error('WechatRegister', 'checkWechatStatus', error); if (error.message.includes('网络连接失败')) { clearTimers(); - sessionIdRef.current = null; // 清理 sessionId - if (isMountedRef.current) { - toast({ - title: "网络连接失败", - description: "请检查网络后重试", - status: "error", - duration: 3000, - isClosable: true, - }); - } + sessionIdRef.current = null; + message.error("网络连接失败,请检查网络后重试"); } } - }, [handleLoginSuccess, clearTimers, toast]); + }, [handleLoginSuccess, clearTimers, authEvents]); - /** - * 启动轮询 - */ const startPolling = useCallback(() => { - // 清理旧的定时器 clearTimers(); - - // 启动轮询 - pollIntervalRef.current = setInterval(() => { - checkWechatStatus(); - }, POLL_INTERVAL); - - // 设置超时 + pollIntervalRef.current = setInterval(checkWechatStatus, POLL_INTERVAL); timeoutRef.current = setTimeout(() => { clearTimers(); - sessionIdRef.current = null; // 清理 sessionId + sessionIdRef.current = null; setWechatStatus(WECHAT_STATUS.EXPIRED); }, QR_CODE_TIMEOUT); }, [checkWechatStatus, clearTimers]); - /** - * 获取微信二维码 - */ const getWechatQRCode = useCallback(async () => { try { setIsLoading(true); - - // 追踪用户选择微信登录(首次或刷新) const isRefresh = Boolean(wechatSessionId); - if (isRefresh) { - const oldSessionId = wechatSessionId; - authEvents.trackWechatLoginInitiated('qr_refresh'); - // 稍后会在成功时追踪刷新事件 - } else { - authEvents.trackWechatLoginInitiated('qr_area'); - } + authEvents.trackWechatLoginInitiated(isRefresh ? 'qr_refresh' : 'qr_area'); - // 生产环境:调用真实 API const response = await authService.getWechatQRCode(); - - // 检查组件是否已卸载 if (!isMountedRef.current) return; - // 安全检查:确保响应包含必要字段 - if (!response) { - throw new Error('服务器无响应'); + if (!response || response.code !== 0) { + throw new Error(response?.message || '获取二维码失败'); } - if (response.code !== 0) { - throw new Error(response.message || '获取二维码失败'); - } - - // 追踪二维码显示 (首次或刷新) if (isRefresh) { authEvents.trackWechatQRRefreshed(wechatSessionId, response.data.session_id); } else { authEvents.trackWechatQRDisplayed(response.data.session_id, response.data.auth_url); } - // 同时更新 ref 和 state,确保轮询能立即读取到最新值 sessionIdRef.current = response.data.session_id; setWechatAuthUrl(response.data.auth_url); setWechatSessionId(response.data.session_id); setWechatStatus(WECHAT_STATUS.WAITING); - - // 启动轮询检查扫码状态 startPolling(); } catch (error) { - // 追踪获取二维码失败 - authEvents.trackError('api', error.message || '获取二维码失败', { - context: 'get_wechat_qrcode' - }); - + authEvents.trackError('api', error.message || '获取二维码失败'); logger.error('WechatRegister', 'getWechatQRCode', error); - if (isMountedRef.current) { - showError("获取微信授权失败", error.message || "请稍后重试"); - } + message.error(error.message || "获取微信授权失败,请稍后重试"); } finally { - if (isMountedRef.current) { - setIsLoading(false); - } + if (isMountedRef.current) setIsLoading(false); } - }, [startPolling, showError, wechatSessionId, authEvents]); + }, [startPolling, wechatSessionId, authEvents]); - /** - * 安全的按钮点击处理,确保所有错误都被捕获,防止被 ErrorBoundary 捕获 - */ const handleGetQRCodeClick = useCallback(async () => { try { await getWechatQRCode(); } catch (error) { - // 错误已经在 getWechatQRCode 中处理,这里只需要防止未捕获的 Promise rejection logger.error('WechatRegister', 'handleGetQRCodeClick', error); } }, [getWechatQRCode]); - /** - * 同步 wechatStatusRef 与 wechatStatus state - * 确保 checkWechatStatus 回调中能获取到最新状态 - */ - useEffect(() => { - wechatStatusRef.current = wechatStatus; - }, [wechatStatus]); + useEffect(() => { wechatStatusRef.current = wechatStatus; }, [wechatStatus]); - /** - * 组件卸载时清理定时器和标记组件状态 - */ useEffect(() => { isMountedRef.current = true; - return () => { isMountedRef.current = false; clearTimers(); - sessionIdRef.current = null; // 清理 sessionId + sessionIdRef.current = null; }; }, [clearTimers]); - /** - * 测量容器尺寸并计算缩放比例 - */ - useLayoutEffect(() => { - // 微信授权页面的原始尺寸(需要与iframe实际尺寸匹配) - const ORIGINAL_WIDTH = 300; // ✅ 修正:与iframe width匹配 - const ORIGINAL_HEIGHT = 350; // ✅ 修正:与iframe height匹配 - - 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 ( - // - // {STATUS_MESSAGES[wechatStatus]} - // - // ); - // }; - return ( - - {/* ========== 标题区域 ========== */} - - 微信登陆 - +
+ 微信登陆 - {/* ========== 二维码区域 ========== */} - +
{wechatStatus === WECHAT_STATUS.WAITING ? ( - /* 已获取二维码:显示iframe */