From ecc3d088a588b33c67f853154d7d16869acbd3c5 Mon Sep 17 00:00:00 2001 From: LostSalt Date: Tue, 24 Mar 2026 20:53:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E6=A0=8F=E7=9A=84=E9=BC=A0=E6=A0=87=E6=82=AC=E5=81=9C=E6=95=88?= =?UTF-8?q?=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/business/UserProfileHeader.tsx | 26 ++++- src/h | 93 ++++++++++++++++ src/hooks/index.ts | 5 + src/hooks/useHover.ts | 93 ++++++++++++++++ src/navigation/DesktopNavigator.tsx | 103 +++++++++++++----- 5 files changed, 289 insertions(+), 31 deletions(-) create mode 100644 src/h create mode 100644 src/hooks/useHover.ts diff --git a/src/components/business/UserProfileHeader.tsx b/src/components/business/UserProfileHeader.tsx index d680ae0..7f54b5f 100644 --- a/src/components/business/UserProfileHeader.tsx +++ b/src/components/business/UserProfileHeader.tsx @@ -20,7 +20,7 @@ import { User } from '../../types'; import Text from '../common/Text'; import Button from '../common/Button'; import Avatar from '../common/Avatar'; -import { useResponsive } from '../../hooks'; +import { useResponsive, useHover } from '../../hooks'; interface UserProfileHeaderProps { user: User; @@ -126,10 +126,15 @@ const UserProfileHeader: React.FC = ({ label: string; onPress?: () => void; }) => { + const { isHovered, hoverProps } = useHover(); + const content = ( - - {value} - + + {value} + {label} @@ -138,9 +143,14 @@ const UserProfileHeader: React.FC = ({ if (onPress) { return ( {content} @@ -473,11 +483,17 @@ const styles = StyleSheet.create({ statItemTouchable: { borderRadius: borderRadius.md, }, + statItemHovered: { + backgroundColor: `${colors.primary.main}08`, + }, statContent: { alignItems: 'center', paddingVertical: spacing.sm, paddingHorizontal: spacing.xs, }, + statContentHovered: { + transform: [{ scale: 1.02 }], + }, statNumber: { fontWeight: '600', marginBottom: 0, diff --git a/src/h b/src/h new file mode 100644 index 0000000..4518e2a --- /dev/null +++ b/src/h @@ -0,0 +1,93 @@ +/** + * useHover Hook + * 用于检测鼠标悬停状态,仅在桌面端(Web)生效 + * 在移动端返回默认值 false + */ +import { useState, useCallback, useRef, useEffect } from 'react'; +import { Platform } from 'react-native'; + +interface UseHoverOptions { + enterDelay?: number; + leaveDelay?: number; +} + +interface UseHoverReturn { + isHovered: boolean; + hoverProps: { + onMouseEnter: () => void; + onMouseLeave: () => void; + }; +} + +/** + * 检测当前是否在 Web 平台 + */ +const isWeb = Platform.OS === 'web'; + +/** + * useHover Hook + * @param options 配置选项 + * @returns 悬停状态和事件处理器 + */ +export function useHover(options: UseHoverOptions = {}): UseHoverReturn { + const { enterDelay = 0, leaveDelay = 0 } = options; + const [isHovered, setIsHovered] = useState(false); + const enterTimeoutRef = useRef(null); + const leaveTimeoutRef = useRef(null); + + // 清理定时器 + useEffect(() => { + return () => { + if (enterTimeoutRef.current) { + clearTimeout(enterTimeoutRef.current); + } + if (leaveTimeoutRef.current) { + clearTimeout(leaveTimeoutRef.current); + } + }; + }, []); + + const handleMouseEnter = useCallback(() => { + if (!isWeb) return; + + if (leaveTimeoutRef.current) { + clearTimeout(leaveTimeoutRef.current); + leaveTimeoutRef.current = null; + } + + if (enterDelay > 0) { + enterTimeoutRef.current = setTimeout(() => { + setIsHovered(true); + }, enterDelay); + } else { + setIsHovered(true); + } + }, [enterDelay]); + + const handleMouseLeave = useCallback(() => { + if (!isWeb) return; + + if (enterTimeoutRef.current) { + clearTimeout(enterTimeoutRef.current); + enterTimeoutRef.current = null; + } + + if (leaveDelay > 0) { + leaveTimeoutRef.current = setTimeout(() => { + setIsHovered(false); + }, leaveDelay); + } else { + setIsHovered(false); + } + }, [leaveDelay]); + + return { + isHovered, + hoverProps: { + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, + }, + }; +} + +export default useHover; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 88416f5..187c978 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -92,3 +92,8 @@ export type { UseConnectionStateResult } from './useConnectionState'; export { useMediaCache } from './useMediaCache'; export type { UseMediaCacheReturn } from './useMediaCache'; + +// ==================== 悬停效果 Hooks ==================== +export { useHover } from './useHover'; + +export type { UseHoverOptions, UseHoverReturn } from './useHover'; diff --git a/src/hooks/useHover.ts b/src/hooks/useHover.ts new file mode 100644 index 0000000..add5472 --- /dev/null +++ b/src/hooks/useHover.ts @@ -0,0 +1,93 @@ +/** + * useHover Hook + * 用于检测鼠标悬停状态,仅在桌面端(Web)生效 + * 在移动端返回默认值 false + */ +import { useState, useCallback, useRef, useEffect } from 'react'; +import { Platform } from 'react-native'; + +export interface UseHoverOptions { + enterDelay?: number; + leaveDelay?: number; +} + +export interface UseHoverReturn { + isHovered: boolean; + hoverProps: { + onMouseEnter: () => void; + onMouseLeave: () => void; + }; +} + +/** + * 检测当前是否在 Web 平台 + */ +const isWeb = Platform.OS === 'web'; + +/** + * useHover Hook + * @param options 配置选项 + * @returns 悬停状态和事件处理器 + */ +export function useHover(options: UseHoverOptions = {}): UseHoverReturn { + const { enterDelay = 0, leaveDelay = 0 } = options; + const [isHovered, setIsHovered] = useState(false); + const enterTimeoutRef = useRef(null); + const leaveTimeoutRef = useRef(null); + + // 清理定时器 + useEffect(() => { + return () => { + if (enterTimeoutRef.current) { + clearTimeout(enterTimeoutRef.current); + } + if (leaveTimeoutRef.current) { + clearTimeout(leaveTimeoutRef.current); + } + }; + }, []); + + const handleMouseEnter = useCallback(() => { + if (!isWeb) return; + + if (leaveTimeoutRef.current) { + clearTimeout(leaveTimeoutRef.current); + leaveTimeoutRef.current = null; + } + + if (enterDelay > 0) { + enterTimeoutRef.current = setTimeout(() => { + setIsHovered(true); + }, enterDelay); + } else { + setIsHovered(true); + } + }, [enterDelay]); + + const handleMouseLeave = useCallback(() => { + if (!isWeb) return; + + if (enterTimeoutRef.current) { + clearTimeout(enterTimeoutRef.current); + enterTimeoutRef.current = null; + } + + if (leaveDelay > 0) { + leaveTimeoutRef.current = setTimeout(() => { + setIsHovered(false); + }, leaveDelay); + } else { + setIsHovered(false); + } + }, [leaveDelay]); + + return { + isHovered, + hoverProps: { + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, + }, + }; +} + +export default useHover; diff --git a/src/navigation/DesktopNavigator.tsx b/src/navigation/DesktopNavigator.tsx index fd77770..c4fbd37 100644 --- a/src/navigation/DesktopNavigator.tsx +++ b/src/navigation/DesktopNavigator.tsx @@ -19,6 +19,7 @@ import type { TabName, NavItemConfig } from '../infrastructure/navigation/types' import { NAVIGATION_CONSTANTS } from '../infrastructure/navigation/types'; import { colors, shadows } from '../theme'; import { useNavigationState } from '../infrastructure/navigation/hooks/useNavigationState'; +import { useHover } from '../hooks/useHover'; // 导入屏幕 import { HomeScreen } from '../screens/home'; @@ -41,6 +42,66 @@ interface DesktopNavigatorProps { unreadCount?: number; } +// 单个导航项组件,支持悬停效果 +interface NavItemProps { + item: NavItemConfig; + isActive: boolean; + isCollapsed: boolean; + showBadge: boolean; + unreadCount: number; + onPress: () => void; +} + +const NavItem: React.FC = ({ + item, + isActive, + isCollapsed, + showBadge, + unreadCount, + onPress, +}) => { + const { isHovered, hoverProps } = useHover(); + + return ( + + + + {showBadge && ( + + {unreadCount > 99 ? '99+' : unreadCount} + + )} + + {!isCollapsed && ( + + {item.label} + + )} + + ); +}; + export function DesktopNavigator({ unreadCount = 0 }: DesktopNavigatorProps) { const insets = useSafeAreaInsets(); const { currentTab, isCollapsed, setCurrentTab, toggleCollapse, setIsReady } = useNavigationState(); @@ -103,34 +164,15 @@ export function DesktopNavigator({ unreadCount = 0 }: DesktopNavigatorProps) { const showBadge = item.name === 'MessageTab' && unreadCount > 0; return ( - handleTabChange(item.name)} - activeOpacity={0.7} - > - - - {showBadge && ( - - {unreadCount > 99 ? '99+' : unreadCount} - - )} - - {!isCollapsed && ( - - {item.label} - - )} - + /> ); })} @@ -201,6 +243,9 @@ const styles = StyleSheet.create({ sidebarItemActive: { backgroundColor: `${colors.primary.main}15`, }, + sidebarItemHovered: { + backgroundColor: `${colors.primary.main}08`, + }, sidebarItemCollapsed: { justifyContent: 'center', paddingHorizontal: 0, @@ -213,6 +258,9 @@ const styles = StyleSheet.create({ justifyContent: 'center', borderRadius: 12, }, + sidebarIconContainerHovered: { + backgroundColor: `${colors.primary.main}10`, + }, sidebarLabel: { fontSize: 15, fontWeight: '500', @@ -224,6 +272,9 @@ const styles = StyleSheet.create({ color: colors.primary.main, fontWeight: '600', }, + sidebarLabelHovered: { + color: colors.primary.main, + }, collapseButton: { flexDirection: 'row', alignItems: 'center',