diff --git a/src/components/Auth/WechatRegister.js b/src/components/Auth/WechatRegister.js index a6f38dec..07af9a66 100644 --- a/src/components/Auth/WechatRegister.js +++ b/src/components/Auth/WechatRegister.js @@ -508,19 +508,20 @@ export default function WechatRegister() { title="微信扫码登录" width="300" height="350" - scrolling="no" // ✅ 新增:禁止滚动 + scrolling="no" // ✅ 新增:禁止滚动 + sandbox="allow-scripts allow-same-origin allow-forms" // ✅ 阻止iframe跳转父页面 style={{ border: 'none', transform: 'scale(0.77) translateY(-35px)', // ✅ 裁剪顶部logo transformOrigin: 'top left', marginLeft: '-5px', pointerEvents: 'auto', // 允许点击 │ │ - overflow: 'hidden', // 尝试隐藏滚动条(可能不起作用) + overflow: 'hidden', // 尝试隐藏滚动条(可能不起作用) }} // 使用 onWheel 事件阻止滚动 │ │ onWheel={(e) => e.preventDefault()} // ✅ 在父容器上阻止滚动 onTouchMove={(e) => e.preventDefault()} // ✅ 移动端也阻止 - + /> ) : ( /* 未获取:显示占位符 */ diff --git a/src/components/Navbars/components/Navigation/DesktopNav.js b/src/components/Navbars/components/Navigation/DesktopNav.js index 642dd4ba..33c91e18 100644 --- a/src/components/Navbars/components/Navigation/DesktopNav.js +++ b/src/components/Navbars/components/Navigation/DesktopNav.js @@ -12,12 +12,12 @@ import { Text, Flex, Badge, - useColorModeValue, - useDisclosure + useColorModeValue } from '@chakra-ui/react'; import { ChevronDownIcon } from '@chakra-ui/icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigationEvents } from '../../../../hooks/useNavigationEvents'; +import { useDelayedMenu } from '../../../../hooks/useDelayedMenu'; /** * 桌面版主导航菜单组件 @@ -37,11 +37,11 @@ const DesktopNav = memo(({ isAuthenticated, user }) => { // 🎯 初始化导航埋点Hook const navEvents = useNavigationEvents({ component: 'top_nav' }); - // 🎯 为每个菜单创建独立的 useDisclosure Hook - const { isOpen: isHighFreqOpen, onOpen: onHighFreqOpen, onClose: onHighFreqClose } = useDisclosure(); - const { isOpen: isMarketReviewOpen, onOpen: onMarketReviewOpen, onClose: onMarketReviewClose } = useDisclosure(); - const { isOpen: isAgentCommunityOpen, onOpen: onAgentCommunityOpen, onClose: onAgentCommunityClose } = useDisclosure(); - const { isOpen: isContactUsOpen, onOpen: onContactUsOpen, onClose: onContactUsClose } = useDisclosure(); + // 🎯 为每个菜单创建延迟关闭控制(200ms 延迟) + const highFreqMenu = useDelayedMenu({ closeDelay: 200 }); + const marketReviewMenu = useDelayedMenu({ closeDelay: 200 }); + const agentCommunityMenu = useDelayedMenu({ closeDelay: 200 }); + const contactUsMenu = useDelayedMenu({ closeDelay: 200 }); // 辅助函数:判断导航项是否激活 const isActive = useCallback((paths) => { @@ -53,7 +53,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => { return ( {/* 高频跟踪 */} - + { borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'} borderColor="blue.600" _hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }} - onMouseEnter={onHighFreqOpen} - onMouseLeave={onHighFreqClose} + onMouseEnter={highFreqMenu.handleMouseEnter} + onMouseLeave={highFreqMenu.handleMouseLeave} + onClick={highFreqMenu.handleClick} > 高频跟踪 - + { // 🎯 追踪菜单项点击 navEvents.trackMenuItemClicked('事件中心', 'dropdown', '/community'); navigate('/community'); - onHighFreqClose(); // 跳转后关闭菜单 + highFreqMenu.onClose(); // 跳转后关闭菜单 }} borderRadius="md" bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'} @@ -96,7 +102,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => { // 🎯 追踪菜单项点击 navEvents.trackMenuItemClicked('概念中心', 'dropdown', '/concepts'); navigate('/concepts'); - onHighFreqClose(); // 跳转后关闭菜单 + highFreqMenu.onClose(); // 跳转后关闭菜单 }} borderRadius="md" bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'} @@ -113,7 +119,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => { {/* 行情复盘 */} - + { borderBottom={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? '2px solid' : 'none'} borderColor="blue.600" _hover={{ bg: isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.100' : 'gray.50' }} - onMouseEnter={onMarketReviewOpen} - onMouseLeave={onMarketReviewClose} + onMouseEnter={marketReviewMenu.handleMouseEnter} + onMouseLeave={marketReviewMenu.handleMouseLeave} + onClick={marketReviewMenu.handleClick} > 行情复盘 - + { navigate('/limit-analyse'); - onMarketReviewClose(); // 跳转后关闭菜单 + marketReviewMenu.onClose(); // 跳转后关闭菜单 }} borderRadius="md" bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'} @@ -149,7 +161,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => { { navigate('/stocks'); - onMarketReviewClose(); // 跳转后关闭菜单 + marketReviewMenu.onClose(); // 跳转后关闭菜单 }} borderRadius="md" bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'} @@ -165,7 +177,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => { { navigate('/trading-simulation'); - onMarketReviewClose(); // 跳转后关闭菜单 + marketReviewMenu.onClose(); // 跳转后关闭菜单 }} borderRadius="md" bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'} @@ -182,17 +194,23 @@ const DesktopNav = memo(({ isAuthenticated, user }) => { {/* AGENT社群 */} - + } - onMouseEnter={onAgentCommunityOpen} - onMouseLeave={onAgentCommunityClose} + onMouseEnter={agentCommunityMenu.handleMouseEnter} + onMouseLeave={agentCommunityMenu.handleMouseLeave} + onClick={agentCommunityMenu.handleClick} > AGENT社群 - + { {/* 联系我们 */} - + } - onMouseEnter={onContactUsOpen} - onMouseLeave={onContactUsClose} + onMouseEnter={contactUsMenu.handleMouseEnter} + onMouseLeave={contactUsMenu.handleMouseLeave} + onClick={contactUsMenu.handleClick} > 联系我们 - + 敬请期待 diff --git a/src/components/Navbars/components/Navigation/MoreMenu.js b/src/components/Navbars/components/Navigation/MoreMenu.js index 4a2293b8..f2edf875 100644 --- a/src/components/Navbars/components/Navigation/MoreMenu.js +++ b/src/components/Navbars/components/Navigation/MoreMenu.js @@ -12,11 +12,11 @@ import { Text, Flex, HStack, - Badge, - useDisclosure + Badge } from '@chakra-ui/react'; import { ChevronDownIcon } from '@chakra-ui/icons'; import { useNavigate, useLocation } from 'react-router-dom'; +import { useDelayedMenu } from '../../../../hooks/useDelayedMenu'; /** * 平板版"更多"下拉菜单组件 @@ -30,8 +30,8 @@ const MoreMenu = memo(({ isAuthenticated, user }) => { const navigate = useNavigate(); const location = useLocation(); - // 🎯 为"更多"菜单创建 useDisclosure Hook - const { isOpen, onOpen, onClose } = useDisclosure(); + // 🎯 使用延迟关闭菜单控制 + const moreMenu = useDelayedMenu({ closeDelay: 200 }); // 辅助函数:判断导航项是否激活 const isActive = useCallback((paths) => { @@ -41,23 +41,29 @@ const MoreMenu = memo(({ isAuthenticated, user }) => { if (!isAuthenticated || !user) return null; return ( - + } fontWeight="medium" - onMouseEnter={onOpen} - onMouseLeave={onClose} + onMouseEnter={moreMenu.handleMouseEnter} + onMouseLeave={moreMenu.handleMouseLeave} + onClick={moreMenu.handleClick} > 更多 - + {/* 高频跟踪组 */} 高频跟踪 { - onClose(); // 先关闭菜单 + moreMenu.onClose(); // 先关闭菜单 navigate('/community'); }} borderRadius="md" @@ -73,7 +79,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => { { - onClose(); // 先关闭菜单 + moreMenu.onClose(); // 先关闭菜单 navigate('/concepts'); }} borderRadius="md" @@ -91,7 +97,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => { 行情复盘 { - onClose(); // 先关闭菜单 + moreMenu.onClose(); // 先关闭菜单 navigate('/limit-analyse'); }} borderRadius="md" @@ -104,7 +110,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => { { - onClose(); // 先关闭菜单 + moreMenu.onClose(); // 先关闭菜单 navigate('/stocks'); }} borderRadius="md" @@ -117,7 +123,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => { { - onClose(); // 先关闭菜单 + moreMenu.onClose(); // 先关闭菜单 navigate('/trading-simulation'); }} borderRadius="md" diff --git a/src/hooks/useDelayedMenu.js b/src/hooks/useDelayedMenu.js new file mode 100644 index 00000000..368a9468 --- /dev/null +++ b/src/hooks/useDelayedMenu.js @@ -0,0 +1,142 @@ +// src/hooks/useDelayedMenu.js +// 导航菜单延迟关闭 Hook - 优化 hover 和 click 交互体验 + +import { useState, useRef, useCallback } from 'react'; + +/** + * 自定义 Hook:提供带延迟关闭功能的菜单控制 + * + * 解决问题: + * 1. 用户快速移动鼠标导致菜单意外关闭 + * 2. Hover 和 Click 状态冲突 + * 3. 从 MenuButton 移动到 MenuList 时菜单闪烁 + * + * 功能特性: + * - ✅ Hover 进入:立即打开菜单 + * - ✅ Hover 离开:延迟关闭(默认 200ms) + * - ✅ Click 切换:支持点击切换打开/关闭状态 + * - ✅ 智能取消:再次 hover 进入时取消关闭定时器 + * + * @param {Object} options - 配置选项 + * @param {number} options.closeDelay - 延迟关闭时间(毫秒),默认 200ms + * @returns {Object} 菜单控制对象 + */ +export function useDelayedMenu({ closeDelay = 200 } = {}) { + const [isOpen, setIsOpen] = useState(false); + const closeTimerRef = useRef(null); + const isClickedRef = useRef(false); // 追踪是否通过点击打开 + + /** + * 打开菜单 + * - 立即打开,无延迟 + * - 清除任何待执行的关闭定时器 + */ + const onOpen = useCallback(() => { + // 清除待执行的关闭定时器 + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + } + setIsOpen(true); + }, []); + + /** + * 延迟关闭菜单 + * - 设置定时器,延迟后关闭 + * - 如果在延迟期间再次 hover 进入,会被 onOpen 取消 + */ + const onDelayedClose = useCallback(() => { + // 如果是点击打开的,hover 离开时不自动关闭 + if (isClickedRef.current) { + return; + } + + // 清除之前的定时器(防止重复设置) + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current); + } + + // 设置延迟关闭定时器 + closeTimerRef.current = setTimeout(() => { + setIsOpen(false); + closeTimerRef.current = null; + }, closeDelay); + }, [closeDelay]); + + /** + * 立即关闭菜单 + * - 无延迟,立即关闭 + * - 清除所有定时器和状态标记 + */ + const onClose = useCallback(() => { + // 清除定时器 + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + } + setIsOpen(false); + isClickedRef.current = false; + }, []); + + /** + * 切换菜单状态(用于点击) + * - 如果关闭 → 打开,并标记为点击打开 + * - 如果打开 → 关闭,并清除点击标记 + */ + const onToggle = useCallback(() => { + if (isOpen) { + // 当前已打开 → 关闭 + onClose(); + } else { + // 当前已关闭 → 打开 + onOpen(); + isClickedRef.current = true; // 标记为点击打开 + } + }, [isOpen, onOpen, onClose]); + + /** + * Hover 进入处理 + * - 打开菜单 + * - 清除点击标记(允许 hover 离开时自动关闭) + */ + const handleMouseEnter = useCallback(() => { + onOpen(); + isClickedRef.current = false; // 清除点击标记,允许 hover 控制 + }, [onOpen]); + + /** + * Hover 离开处理 + * - 延迟关闭菜单 + */ + const handleMouseLeave = useCallback(() => { + onDelayedClose(); + }, [onDelayedClose]); + + /** + * 点击处理 + * - 切换菜单状态 + */ + const handleClick = useCallback(() => { + onToggle(); + }, [onToggle]); + + // 组件卸载时清理定时器 + const cleanup = useCallback(() => { + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + } + }, []); + + return { + isOpen, + onOpen, + onClose, + onDelayedClose, + onToggle, + handleMouseEnter, + handleMouseLeave, + handleClick, + cleanup + }; +}