fix(UserMenu): 修复 Phase 3 重构引入的头像 UI 问题

**问题描述**
Phase 3 重构提取用户菜单组件时,引入了多个 UI 和交互问题:
1.  皇冠 UI 改变:右上角 FaCrown → 左上角 Emoji
2.  Hover 效果消失:平板版头像无 hover
3.  Tooltip 内容丢失:简化版内容 → 原始丰富内容
4.  Tooltip 不显示:Chakra UI ref 传递问题
5. ⚠️ React 警告:forwardRef 缺失

**修复内容**

### 1. UserAvatar.js (101行 → 76行, -25行)

**恢复原始皇冠设计**:
- 删除自定义 CrownIcon(FaCrown + 渐变背景)
- 改用 CrownTooltip.js 原始实现(👑/💎 Emoji)
- 位置:右上角 → 左上角
- 交互:无 → 有 scale(1.2) hover

**修复 Hover 效果**:
```diff
- _hover={onClick ? { ...defaultHoverStyle, ...hoverStyle } : undefined}
+ _hover={{ ...defaultHoverStyle, ...hoverStyle }}
```
- 移除 onClick 依赖,头像始终可交互

**添加 forwardRef**:
```diff
- const UserAvatar = memo(({ user, subscriptionInfo, ... }) => {
+ const UserAvatar = forwardRef(({ user, subscriptionInfo, ... }, ref) => {
+     return <Box ref={ref} ...>
```
- 支持 Tooltip 和 MenuButton 传递 ref
- 消除 React 控制台警告

### 2. DesktopUserMenu.js (93行 → 65行, -28行)

**恢复原始 TooltipContent**:
```diff
- const TooltipContent = memo(({ subscriptionInfo }) => {
-     return getSubscriptionBadgeText(); // 纯文本
- });
+ import { TooltipContent } from '../../../Subscription/CrownTooltip';
```
- 恢复丰富 UI:VStack + Divider + 状态图标 + 剩余天数
- 支持紧急提醒(< 7天)和警告(< 30天)

**修复 Tooltip 显示**:
```diff
  <Tooltip ...>
+     <span>
          <UserAvatar ... />
+     </span>
  </Tooltip>
```
- 添加 span 包裹层确保 ref 和事件正确传递
- Chakra UI 官方推荐做法

**修复验证**
-  桌面版:皇冠在左上角(👑/💎),Tooltip 显示丰富内容
-  平板版:头像有 hover 效果,下拉菜单正常
-  控制台:无 forwardRef 警告

**测试场景**
1. 免费用户:无皇冠,Tooltip 显示升级提示
2. Pro/Max 用户:显示皇冠,Tooltip 显示剩余天数
3. < 7天到期:红色紧急提示
4. 已过期:显示续费提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-10-30 18:27:55 +08:00
parent 3b0146fe49
commit 56003039bd
2 changed files with 21 additions and 76 deletions

View File

@@ -5,40 +5,9 @@ import React, { memo } from 'react';
import { Tooltip, useColorModeValue } from '@chakra-ui/react'; import { Tooltip, useColorModeValue } from '@chakra-ui/react';
import UserAvatar from './UserAvatar'; import UserAvatar from './UserAvatar';
import SubscriptionModal from '../../../Subscription/SubscriptionModal'; import SubscriptionModal from '../../../Subscription/SubscriptionModal';
import { TooltipContent } from '../../../Subscription/CrownTooltip';
import { useSubscription } from '../../../../hooks/useSubscription'; import { useSubscription } from '../../../../hooks/useSubscription';
/**
* Tooltip 内容组件
* 显示用户订阅信息和剩余天数
*/
const TooltipContent = memo(({ subscriptionInfo }) => {
const getSubscriptionBadgeText = () => {
if (!subscriptionInfo || !subscriptionInfo.type) {
return '免费版';
}
const type = subscriptionInfo.type.toLowerCase();
switch (type) {
case 'max':
return subscriptionInfo.is_active
? `Max版 (剩余 ${subscriptionInfo.days_left || 0} 天)`
: 'Max版 (已过期)';
case 'pro':
return subscriptionInfo.is_active
? `Pro版 (剩余 ${subscriptionInfo.days_left || 0} 天)`
: 'Pro版 (已过期)';
case 'free':
default:
return '免费版 (点击升级)';
}
};
return getSubscriptionBadgeText();
});
TooltipContent.displayName = 'TooltipContent';
/** /**
* 桌面版用户菜单组件 * 桌面版用户菜单组件
* 大屏幕 (md+) 显示,头像点击打开订阅弹窗 * 大屏幕 (md+) 显示,头像点击打开订阅弹窗
@@ -70,11 +39,13 @@ const DesktopUserMenu = memo(({ user }) => {
boxShadow="lg" boxShadow="lg"
p={3} p={3}
> >
<UserAvatar <span>
user={user} <UserAvatar
subscriptionInfo={subscriptionInfo} user={user}
onClick={openSubscriptionModal} subscriptionInfo={subscriptionInfo}
/> onClick={openSubscriptionModal}
/>
</span>
</Tooltip> </Tooltip>
{isSubscriptionModalOpen && ( {isSubscriptionModalOpen && (

View File

@@ -1,41 +1,9 @@
// src/components/Navbars/components/UserMenu/UserAvatar.js // src/components/Navbars/components/UserMenu/UserAvatar.js
// 用户头像组件 - 带皇冠图标和订阅边框 // 用户头像组件 - 带皇冠图标和订阅边框
import React, { memo } from 'react'; import React, { memo, forwardRef } from 'react';
import { Box, Avatar } from '@chakra-ui/react'; import { Box, Avatar } from '@chakra-ui/react';
import { FaCrown } from 'react-icons/fa'; import { CrownIcon } from '../../../Subscription/CrownTooltip';
/**
* 皇冠图标组件
* @param {Object} props.subscriptionInfo - 订阅信息
*/
const CrownIcon = memo(({ subscriptionInfo }) => {
if (!subscriptionInfo || subscriptionInfo.type === 'free') {
return null;
}
const crownColor = subscriptionInfo.type === 'max'
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: '#667eea';
return (
<Box
position="absolute"
top="-4px"
right="-4px"
zIndex={2}
fontSize="14px"
background={crownColor}
borderRadius="full"
p="3px"
boxShadow="0 2px 8px rgba(102, 126, 234, 0.4)"
>
<FaCrown color="white" />
</Box>
);
});
CrownIcon.displayName = 'CrownIcon';
/** /**
* 用户头像组件 * 用户头像组件
@@ -47,14 +15,15 @@ CrownIcon.displayName = 'CrownIcon';
* @param {string} props.size - 头像大小 (默认 'sm') * @param {string} props.size - 头像大小 (默认 'sm')
* @param {Function} props.onClick - 点击回调 * @param {Function} props.onClick - 点击回调
* @param {Object} props.hoverStyle - 悬停样式 * @param {Object} props.hoverStyle - 悬停样式
* @param {React.Ref} ref - 用于 Tooltip 和 MenuButton 的 ref
*/ */
const UserAvatar = memo(({ const UserAvatar = forwardRef(({
user, user,
subscriptionInfo, subscriptionInfo,
size = 'sm', size = 'sm',
onClick, onClick,
hoverStyle = {} hoverStyle = {}
}) => { }, ref) => {
// 获取显示名称 // 获取显示名称
const getDisplayName = () => { const getDisplayName = () => {
if (user.nickname) return user.nickname; if (user.nickname) return user.nickname;
@@ -71,7 +40,7 @@ const UserAvatar = memo(({
return 'transparent'; return 'transparent';
}; };
// 默认悬停样式 // 默认悬停样式 - 头像始终可交互(在 Tooltip 或 MenuButton 中)
const defaultHoverStyle = { const defaultHoverStyle = {
transform: 'scale(1.05)', transform: 'scale(1.05)',
boxShadow: subscriptionInfo.type !== 'free' boxShadow: subscriptionInfo.type !== 'free'
@@ -80,7 +49,12 @@ const UserAvatar = memo(({
}; };
return ( return (
<Box position="relative" cursor={onClick ? 'pointer' : 'default'} onClick={onClick}> <Box
ref={ref}
position="relative"
cursor="pointer"
onClick={onClick}
>
<CrownIcon subscriptionInfo={subscriptionInfo} /> <CrownIcon subscriptionInfo={subscriptionInfo} />
<Avatar <Avatar
size={size} size={size}
@@ -89,7 +63,7 @@ const UserAvatar = memo(({
bg="blue.500" bg="blue.500"
border={subscriptionInfo.type !== 'free' ? '2.5px solid' : 'none'} border={subscriptionInfo.type !== 'free' ? '2.5px solid' : 'none'}
borderColor={getBorderColor()} borderColor={getBorderColor()}
_hover={onClick ? { ...defaultHoverStyle, ...hoverStyle } : undefined} _hover={{ ...defaultHoverStyle, ...hoverStyle }}
transition="all 0.2s" transition="all 0.2s"
/> />
</Box> </Box>