feat: bugfix
This commit is contained in:
@@ -509,6 +509,7 @@ export default function WechatRegister() {
|
|||||||
width="300"
|
width="300"
|
||||||
height="350"
|
height="350"
|
||||||
scrolling="no" // ✅ 新增:禁止滚动
|
scrolling="no" // ✅ 新增:禁止滚动
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms" // ✅ 阻止iframe跳转父页面
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
transform: 'scale(0.77) translateY(-35px)', // ✅ 裁剪顶部logo
|
transform: 'scale(0.77) translateY(-35px)', // ✅ 裁剪顶部logo
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Flex,
|
Flex,
|
||||||
Badge,
|
Badge,
|
||||||
useColorModeValue,
|
useColorModeValue
|
||||||
useDisclosure
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useNavigationEvents } from '../../../../hooks/useNavigationEvents';
|
import { useNavigationEvents } from '../../../../hooks/useNavigationEvents';
|
||||||
|
import { useDelayedMenu } from '../../../../hooks/useDelayedMenu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 桌面版主导航菜单组件
|
* 桌面版主导航菜单组件
|
||||||
@@ -37,11 +37,11 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
// 🎯 初始化导航埋点Hook
|
// 🎯 初始化导航埋点Hook
|
||||||
const navEvents = useNavigationEvents({ component: 'top_nav' });
|
const navEvents = useNavigationEvents({ component: 'top_nav' });
|
||||||
|
|
||||||
// 🎯 为每个菜单创建独立的 useDisclosure Hook
|
// 🎯 为每个菜单创建延迟关闭控制(200ms 延迟)
|
||||||
const { isOpen: isHighFreqOpen, onOpen: onHighFreqOpen, onClose: onHighFreqClose } = useDisclosure();
|
const highFreqMenu = useDelayedMenu({ closeDelay: 200 });
|
||||||
const { isOpen: isMarketReviewOpen, onOpen: onMarketReviewOpen, onClose: onMarketReviewClose } = useDisclosure();
|
const marketReviewMenu = useDelayedMenu({ closeDelay: 200 });
|
||||||
const { isOpen: isAgentCommunityOpen, onOpen: onAgentCommunityOpen, onClose: onAgentCommunityClose } = useDisclosure();
|
const agentCommunityMenu = useDelayedMenu({ closeDelay: 200 });
|
||||||
const { isOpen: isContactUsOpen, onOpen: onContactUsOpen, onClose: onContactUsClose } = useDisclosure();
|
const contactUsMenu = useDelayedMenu({ closeDelay: 200 });
|
||||||
|
|
||||||
// 辅助函数:判断导航项是否激活
|
// 辅助函数:判断导航项是否激活
|
||||||
const isActive = useCallback((paths) => {
|
const isActive = useCallback((paths) => {
|
||||||
@@ -53,7 +53,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
return (
|
return (
|
||||||
<HStack spacing={8}>
|
<HStack spacing={8}>
|
||||||
{/* 高频跟踪 */}
|
{/* 高频跟踪 */}
|
||||||
<Menu isOpen={isHighFreqOpen} onClose={onHighFreqClose}>
|
<Menu isOpen={highFreqMenu.isOpen} onClose={highFreqMenu.onClose}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -64,18 +64,24 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'}
|
borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'}
|
||||||
borderColor="blue.600"
|
borderColor="blue.600"
|
||||||
_hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }}
|
_hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }}
|
||||||
onMouseEnter={onHighFreqOpen}
|
onMouseEnter={highFreqMenu.handleMouseEnter}
|
||||||
onMouseLeave={onHighFreqClose}
|
onMouseLeave={highFreqMenu.handleMouseLeave}
|
||||||
|
onClick={highFreqMenu.handleClick}
|
||||||
>
|
>
|
||||||
高频跟踪
|
高频跟踪
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList minW="260px" p={2} onMouseEnter={onHighFreqOpen}>
|
<MenuList
|
||||||
|
minW="260px"
|
||||||
|
p={2}
|
||||||
|
onMouseEnter={highFreqMenu.handleMouseEnter}
|
||||||
|
onMouseLeave={highFreqMenu.handleMouseLeave}
|
||||||
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// 🎯 追踪菜单项点击
|
// 🎯 追踪菜单项点击
|
||||||
navEvents.trackMenuItemClicked('事件中心', 'dropdown', '/community');
|
navEvents.trackMenuItemClicked('事件中心', 'dropdown', '/community');
|
||||||
navigate('/community');
|
navigate('/community');
|
||||||
onHighFreqClose(); // 跳转后关闭菜单
|
highFreqMenu.onClose(); // 跳转后关闭菜单
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
||||||
@@ -96,7 +102,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
// 🎯 追踪菜单项点击
|
// 🎯 追踪菜单项点击
|
||||||
navEvents.trackMenuItemClicked('概念中心', 'dropdown', '/concepts');
|
navEvents.trackMenuItemClicked('概念中心', 'dropdown', '/concepts');
|
||||||
navigate('/concepts');
|
navigate('/concepts');
|
||||||
onHighFreqClose(); // 跳转后关闭菜单
|
highFreqMenu.onClose(); // 跳转后关闭菜单
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
||||||
@@ -113,7 +119,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{/* 行情复盘 */}
|
{/* 行情复盘 */}
|
||||||
<Menu isOpen={isMarketReviewOpen} onClose={onMarketReviewClose}>
|
<Menu isOpen={marketReviewMenu.isOpen} onClose={marketReviewMenu.onClose}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -124,16 +130,22 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
borderBottom={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? '2px solid' : 'none'}
|
borderBottom={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? '2px solid' : 'none'}
|
||||||
borderColor="blue.600"
|
borderColor="blue.600"
|
||||||
_hover={{ bg: isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.100' : 'gray.50' }}
|
_hover={{ bg: isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.100' : 'gray.50' }}
|
||||||
onMouseEnter={onMarketReviewOpen}
|
onMouseEnter={marketReviewMenu.handleMouseEnter}
|
||||||
onMouseLeave={onMarketReviewClose}
|
onMouseLeave={marketReviewMenu.handleMouseLeave}
|
||||||
|
onClick={marketReviewMenu.handleClick}
|
||||||
>
|
>
|
||||||
行情复盘
|
行情复盘
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList minW="260px" p={2} onMouseEnter={onMarketReviewOpen}>
|
<MenuList
|
||||||
|
minW="260px"
|
||||||
|
p={2}
|
||||||
|
onMouseEnter={marketReviewMenu.handleMouseEnter}
|
||||||
|
onMouseLeave={marketReviewMenu.handleMouseLeave}
|
||||||
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/limit-analyse');
|
navigate('/limit-analyse');
|
||||||
onMarketReviewClose(); // 跳转后关闭菜单
|
marketReviewMenu.onClose(); // 跳转后关闭菜单
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
||||||
@@ -149,7 +161,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/stocks');
|
navigate('/stocks');
|
||||||
onMarketReviewClose(); // 跳转后关闭菜单
|
marketReviewMenu.onClose(); // 跳转后关闭菜单
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
||||||
@@ -165,7 +177,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/trading-simulation');
|
navigate('/trading-simulation');
|
||||||
onMarketReviewClose(); // 跳转后关闭菜单
|
marketReviewMenu.onClose(); // 跳转后关闭菜单
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
||||||
@@ -182,17 +194,23 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{/* AGENT社群 */}
|
{/* AGENT社群 */}
|
||||||
<Menu isOpen={isAgentCommunityOpen} onClose={onAgentCommunityClose}>
|
<Menu isOpen={agentCommunityMenu.isOpen} onClose={agentCommunityMenu.onClose}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
rightIcon={<ChevronDownIcon />}
|
rightIcon={<ChevronDownIcon />}
|
||||||
onMouseEnter={onAgentCommunityOpen}
|
onMouseEnter={agentCommunityMenu.handleMouseEnter}
|
||||||
onMouseLeave={onAgentCommunityClose}
|
onMouseLeave={agentCommunityMenu.handleMouseLeave}
|
||||||
|
onClick={agentCommunityMenu.handleClick}
|
||||||
>
|
>
|
||||||
AGENT社群
|
AGENT社群
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList minW="300px" p={4} onMouseEnter={onAgentCommunityOpen}>
|
<MenuList
|
||||||
|
minW="300px"
|
||||||
|
p={4}
|
||||||
|
onMouseEnter={agentCommunityMenu.handleMouseEnter}
|
||||||
|
onMouseLeave={agentCommunityMenu.handleMouseLeave}
|
||||||
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
isDisabled
|
isDisabled
|
||||||
cursor="not-allowed"
|
cursor="not-allowed"
|
||||||
@@ -211,17 +229,23 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{/* 联系我们 */}
|
{/* 联系我们 */}
|
||||||
<Menu isOpen={isContactUsOpen} onClose={onContactUsClose}>
|
<Menu isOpen={contactUsMenu.isOpen} onClose={contactUsMenu.onClose}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
rightIcon={<ChevronDownIcon />}
|
rightIcon={<ChevronDownIcon />}
|
||||||
onMouseEnter={onContactUsOpen}
|
onMouseEnter={contactUsMenu.handleMouseEnter}
|
||||||
onMouseLeave={onContactUsClose}
|
onMouseLeave={contactUsMenu.handleMouseLeave}
|
||||||
|
onClick={contactUsMenu.handleClick}
|
||||||
>
|
>
|
||||||
联系我们
|
联系我们
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList minW="260px" p={4} onMouseEnter={onContactUsOpen}>
|
<MenuList
|
||||||
|
minW="260px"
|
||||||
|
p={4}
|
||||||
|
onMouseEnter={contactUsMenu.handleMouseEnter}
|
||||||
|
onMouseLeave={contactUsMenu.handleMouseLeave}
|
||||||
|
>
|
||||||
<Text fontSize="sm" color={contactTextColor}>敬请期待</Text>
|
<Text fontSize="sm" color={contactTextColor}>敬请期待</Text>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Flex,
|
Flex,
|
||||||
HStack,
|
HStack,
|
||||||
Badge,
|
Badge
|
||||||
useDisclosure
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
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 navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// 🎯 为"更多"菜单创建 useDisclosure Hook
|
// 🎯 使用延迟关闭菜单控制
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const moreMenu = useDelayedMenu({ closeDelay: 200 });
|
||||||
|
|
||||||
// 辅助函数:判断导航项是否激活
|
// 辅助函数:判断导航项是否激活
|
||||||
const isActive = useCallback((paths) => {
|
const isActive = useCallback((paths) => {
|
||||||
@@ -41,23 +41,29 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
|||||||
if (!isAuthenticated || !user) return null;
|
if (!isAuthenticated || !user) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu isOpen={isOpen} onClose={onClose}>
|
<Menu isOpen={moreMenu.isOpen} onClose={moreMenu.onClose}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
rightIcon={<ChevronDownIcon />}
|
rightIcon={<ChevronDownIcon />}
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
onMouseEnter={onOpen}
|
onMouseEnter={moreMenu.handleMouseEnter}
|
||||||
onMouseLeave={onClose}
|
onMouseLeave={moreMenu.handleMouseLeave}
|
||||||
|
onClick={moreMenu.handleClick}
|
||||||
>
|
>
|
||||||
更多
|
更多
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList minW="300px" p={2} onMouseEnter={onOpen}>
|
<MenuList
|
||||||
|
minW="300px"
|
||||||
|
p={2}
|
||||||
|
onMouseEnter={moreMenu.handleMouseEnter}
|
||||||
|
onMouseLeave={moreMenu.handleMouseLeave}
|
||||||
|
>
|
||||||
{/* 高频跟踪组 */}
|
{/* 高频跟踪组 */}
|
||||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">高频跟踪</Text>
|
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">高频跟踪</Text>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose(); // 先关闭菜单
|
moreMenu.onClose(); // 先关闭菜单
|
||||||
navigate('/community');
|
navigate('/community');
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
@@ -73,7 +79,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose(); // 先关闭菜单
|
moreMenu.onClose(); // 先关闭菜单
|
||||||
navigate('/concepts');
|
navigate('/concepts');
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
@@ -91,7 +97,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
|||||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">行情复盘</Text>
|
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">行情复盘</Text>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose(); // 先关闭菜单
|
moreMenu.onClose(); // 先关闭菜单
|
||||||
navigate('/limit-analyse');
|
navigate('/limit-analyse');
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
@@ -104,7 +110,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose(); // 先关闭菜单
|
moreMenu.onClose(); // 先关闭菜单
|
||||||
navigate('/stocks');
|
navigate('/stocks');
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
@@ -117,7 +123,7 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose(); // 先关闭菜单
|
moreMenu.onClose(); // 先关闭菜单
|
||||||
navigate('/trading-simulation');
|
navigate('/trading-simulation');
|
||||||
}}
|
}}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
|
|||||||
142
src/hooks/useDelayedMenu.js
Normal file
142
src/hooks/useDelayedMenu.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user