修复 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 嵌套警告 - ✅ 点击"取消"功能正常 - ✅ 悬停效果正常显示
181 lines
8.6 KiB
JavaScript
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;
|