Files
vf_react/src/components/Navbars/components/FeatureMenus/WatchlistMenu.js
zdl 20cb83b792 fix: 修复 FeatureMenus 中的按钮嵌套警告
修复 React DOM 嵌套警告:<button> 不能作为 <button> 的后代

**问题描述**
- MenuItem 组件渲染为 <button> 元素
- 在 MenuItem 内使用 Button 组件会导致 button-in-button 嵌套警告

**修复内容**
1. FollowingEventsMenu.js (lines 134-150)
   - 将"取消"按钮从 Button 组件改为 Box 组件
   - 使用 Box + 样式模拟按钮外观和交互

2. WatchlistMenu.js (lines 116-132)
   - 同样将"取消"按钮改为 Box 组件
   - 保持一致的样式和交互行为

**技术方案**
- Box as="span" 渲染为行内元素
- 通过 cursor="pointer" + _hover 实现按钮交互
- 通过 color + borderRadius 实现按钮视觉效果

**测试**
-  控制台无 DOM 嵌套警告
-  点击"取消"功能正常
-  悬停效果正常显示
2025-10-30 18:14:10 +08:00

181 lines
8.6 KiB
JavaScript

// src/components/Navbars/components/FeatureMenus/WatchlistMenu.js
// 自选股下拉菜单组件
import React, { memo } from 'react';
import {
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
Button,
Badge,
Box,
Text,
HStack,
VStack,
Spinner,
useColorModeValue
} from '@chakra-ui/react';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { FiStar } from 'react-icons/fi';
import { useNavigate } from 'react-router-dom';
import { useWatchlist } from '../../../../hooks/useWatchlist';
/**
* 自选股下拉菜单组件
* 显示用户自选股实时行情,支持分页和移除
* 仅在桌面版 (lg+) 显示
*/
const WatchlistMenu = memo(() => {
const navigate = useNavigate();
const {
watchlistQuotes,
watchlistLoading,
watchlistPage,
setWatchlistPage,
WATCHLIST_PAGE_SIZE,
loadWatchlistQuotes,
handleRemoveFromWatchlist
} = useWatchlist();
const titleColor = useColorModeValue('gray.600', 'gray.300');
const loadingTextColor = useColorModeValue('gray.500', 'gray.300');
const emptyTextColor = useColorModeValue('gray.500', 'gray.300');
const codeTextColor = useColorModeValue('gray.500', 'gray.400');
const pageTextColor = useColorModeValue('gray.600', 'gray.400');
return (
<Menu onOpen={loadWatchlistQuotes}>
<MenuButton
as={Button}
size="sm"
colorScheme="teal"
variant="solid"
borderRadius="full"
rightIcon={<ChevronDownIcon />}
leftIcon={<FiStar />}
>
自选股
{watchlistQuotes && watchlistQuotes.length > 0 && (
<Badge ml={2} colorScheme="whiteAlpha">{watchlistQuotes.length}</Badge>
)}
</MenuButton>
<MenuList minW="380px">
<Box px={4} py={2}>
<Text fontSize="sm" color={titleColor}>我的自选股</Text>
</Box>
{watchlistLoading ? (
<Box px={4} py={3}>
<HStack>
<Spinner size="sm" />
<Text fontSize="sm" color={loadingTextColor}>加载中...</Text>
</HStack>
</Box>
) : (
<>
{(!watchlistQuotes || watchlistQuotes.length === 0) ? (
<Box px={4} py={3}>
<Text fontSize="sm" color={emptyTextColor}>暂无自选股</Text>
</Box>
) : (
<VStack align="stretch" spacing={1} px={2} py={1}>
{watchlistQuotes
.slice((watchlistPage - 1) * WATCHLIST_PAGE_SIZE, watchlistPage * WATCHLIST_PAGE_SIZE)
.map((item) => (
<MenuItem
key={item.stock_code}
_hover={{ bg: 'gray.50' }}
onClick={() => navigate(`/company?scode=${item.stock_code}`)}
>
<HStack justify="space-between" w="100%">
<Box>
<Text fontSize="sm" fontWeight="medium">
{item.stock_name || item.stock_code}
</Text>
<Text fontSize="xs" color={codeTextColor}>
{item.stock_code}
</Text>
</Box>
<HStack>
<Badge
colorScheme={
(item.change_percent || 0) > 0 ? 'red' :
((item.change_percent || 0) < 0 ? 'green' : 'gray')
}
fontSize="xs"
>
{(item.change_percent || 0) > 0 ? '+' : ''}
{(item.change_percent || 0).toFixed(2)}%
</Badge>
<Text fontSize="sm">
{item.current_price?.toFixed ?
item.current_price.toFixed(2) :
(item.current_price || '-')}
</Text>
<Box
as="span"
fontSize="xs"
color="red.500"
cursor="pointer"
px={2}
py={1}
borderRadius="md"
_hover={{ bg: 'red.50' }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromWatchlist(item.stock_code);
}}
>
取消
</Box>
</HStack>
</HStack>
</MenuItem>
))}
</VStack>
)}
<MenuDivider />
<HStack justify="space-between" px={3} py={2}>
<HStack>
<Button
size="xs"
variant="outline"
onClick={() => setWatchlistPage((p) => Math.max(1, p - 1))}
isDisabled={watchlistPage <= 1}
>
上一页
</Button>
<Text fontSize="xs" color={pageTextColor}>
{watchlistPage} / {Math.max(1, Math.ceil((watchlistQuotes?.length || 0) / WATCHLIST_PAGE_SIZE))}
</Text>
<Button
size="xs"
variant="outline"
onClick={() => setWatchlistPage((p) =>
Math.min(Math.ceil((watchlistQuotes?.length || 0) / WATCHLIST_PAGE_SIZE) || 1, p + 1)
)}
isDisabled={watchlistPage >= Math.ceil((watchlistQuotes?.length || 0) / WATCHLIST_PAGE_SIZE)}
>
下一页
</Button>
</HStack>
<HStack>
<Button size="xs" variant="ghost" onClick={loadWatchlistQuotes}>刷新</Button>
<Button size="xs" colorScheme="teal" variant="ghost" onClick={() => navigate('/home/center')}>
查看全部
</Button>
</HStack>
</HStack>
</>
)}
</MenuList>
</Menu>
);
});
WatchlistMenu.displayName = 'WatchlistMenu';
export default WatchlistMenu;