Files
vf_react/src/components/Navbars/HomeNavbar.js
2025-10-15 13:44:23 +08:00

977 lines
53 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useState } from 'react';
import {
Box,
Flex,
Text,
Button,
Container,
useDisclosure,
Drawer,
DrawerBody,
DrawerHeader,
DrawerOverlay,
DrawerContent,
DrawerCloseButton,
VStack,
HStack,
Icon,
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
Badge,
Grid,
IconButton,
useBreakpointValue,
Link,
Divider,
Avatar,
Spinner,
useColorMode,
useColorModeValue,
useToast,
} from '@chakra-ui/react';
import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons';
import { FiStar, FiCalendar } from 'react-icons/fi';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
/** 桌面端导航 - 完全按照原网站
* @TODO 添加逻辑 不展示导航case
* 1.未登陆状态 && 是首页
* 2. !isMobile
*/
const NavItems = ({ isAuthenticated, user }) => {
const navigate = useNavigate();
if (!isAuthenticated && !user) {
return (
<HStack spacing={8}>
<Menu>
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
高频跟踪
</MenuButton>
<MenuList minW="260px" p={2}>
<VStack spacing={1} align="stretch">
<Link
onClick={() => navigate('/community')}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<Flex justify="space-between" align="center">
<Text fontSize="sm">新闻催化分析</Text>
<HStack spacing={1}>
<Badge size="sm" colorScheme="green">HOT</Badge>
<Badge size="sm" colorScheme="red">NEW</Badge>
</HStack>
</Flex>
</Link>
<Link
onClick={() => navigate('/concepts')}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<Flex justify="space-between" align="center">
<Text fontSize="sm">概念中心</Text>
<Badge size="sm" colorScheme="red">NEW</Badge>
</Flex>
</Link>
</VStack>
</MenuList>
</Menu>
<Menu>
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
行情复盘
</MenuButton>
<MenuList minW="260px" p={2}>
<VStack spacing={1} align="stretch">
<Link
onClick={() => navigate('/limit-analyse')}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<Flex justify="space-between" align="center">
<Text fontSize="sm">涨停分析</Text>
<Badge size="sm" colorScheme="blue">FREE</Badge>
</Flex>
</Link>
<Link
onClick={() => navigate('/stocks')}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<Flex justify="space-between" align="center">
<Text fontSize="sm">个股中心</Text>
<Badge size="sm" colorScheme="green">HOT</Badge>
</Flex>
</Link>
<Link
href="https://valuefrontier.cn/trading-simulation"
isExternal
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
>
<Flex justify="space-between" align="center">
<Text fontSize="sm">模拟盘</Text>
<Badge size="sm" colorScheme="red">NEW</Badge>
</Flex>
</Link>
</VStack>
</MenuList>
</Menu>
{false && isAuthenticated && (
<Menu onOpen={loadWatchlistQuotes} closeOnSelect={false}>
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>自选股</MenuButton>
</Menu>
)}
<Menu>
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
AGENT社群
</MenuButton>
<MenuList minW="300px" p={4}>
<VStack spacing={2} align="stretch">
<Link
py={2}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">今日热议</Text>
</Link>
<Link
py={2}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">个股社区</Text>
</Link>
</VStack>
</MenuList>
</Menu>
{false && isAuthenticated && (
<Menu onOpen={loadFollowingEvents} closeOnSelect={false}>
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>关注的事件</MenuButton>
</Menu>
)}
<Menu>
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
联系我们
</MenuButton>
<MenuList minW="260px" p={4}>
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.300')}>敬请期待</Text>
</MenuList>
</Menu>
</HStack>
)
} else {
return null;
}
}
export default function HomeNavbar() {
const { isOpen, onOpen, onClose } = useDisclosure();
const navigate = useNavigate();
const isMobile = useBreakpointValue({ base: true, md: false });
const { user, isAuthenticated, logout, isLoading } = useAuth();
const { colorMode, toggleColorMode } = useColorMode();
const navbarBg = useColorModeValue('white', 'gray.800');
const navbarBorder = useColorModeValue('gray.200', 'gray.700');
const brandText = useColorModeValue('gray.800', 'white');
const brandHover = useColorModeValue('blue.600', 'blue.300');
const toast = useToast();
// 添加调试信息
console.log('HomeNavbar Debug:', {
user,
isAuthenticated,
isLoading,
userKeys: user ? Object.keys(user) : 'no user'
});
// 获取显示名称的函数
const getDisplayName = () => {
if (!user) return '';
return user.nickname || user.username || user.name || user.email || '用户';
};
// 处理登出
const handleLogout = async () => {
try {
await logout();
// logout函数已经包含了跳转逻辑这里不需要额外处理
} catch (error) {
console.error('Logout error:', error);
}
};
// 处理登录按钮点击
const handleLoginClick = () => {
navigate('/auth/signin');
};
// 检查是否为禁用的链接没有NEW标签的链接
// const isDisabledLink = true;
// 自选股 / 关注事件 下拉所需状态
const [watchlistQuotes, setWatchlistQuotes] = useState([]);
const [watchlistLoading, setWatchlistLoading] = useState(false);
const [followingEvents, setFollowingEvents] = useState([]);
const [eventsLoading, setEventsLoading] = useState(false);
const [watchlistPage, setWatchlistPage] = useState(1);
const [eventsPage, setEventsPage] = useState(1);
const WATCHLIST_PAGE_SIZE = 10;
const EVENTS_PAGE_SIZE = 8;
// 用户信息完整性状态
const [profileCompleteness, setProfileCompleteness] = useState(null);
const [showCompletenessAlert, setShowCompletenessAlert] = useState(false);
// 计算 API 基础地址(与 Center.js 一致的策略)
const getApiBase = () => (process.env.NODE_ENV === 'production' ? '' : (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'));
const loadWatchlistQuotes = useCallback(async () => {
try {
setWatchlistLoading(true);
const base = getApiBase();
const resp = await fetch(base + '/api/account/watchlist/realtime', {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
if (resp.ok) {
const data = await resp.json();
if (data && data.success && Array.isArray(data.data)) {
setWatchlistQuotes(data.data);
} else {
setWatchlistQuotes([]);
}
} else {
setWatchlistQuotes([]);
}
} catch (e) {
console.warn('加载自选股实时行情失败:', e);
setWatchlistQuotes([]);
} finally {
setWatchlistLoading(false);
}
}, []);
const loadFollowingEvents = useCallback(async () => {
try {
setEventsLoading(true);
const base = getApiBase();
const resp = await fetch(base + '/api/account/events/following', {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
if (resp.ok) {
const data = await resp.json();
if (data && data.success && Array.isArray(data.data)) {
const ids = data.data.map((e) => e.id).filter(Boolean);
if (ids.length === 0) {
setFollowingEvents([]);
} else {
// 并行请求详情以获取涨幅字段
const detailResponses = await Promise.all(ids.map((id) => fetch(base + `/api/events/${id}`, {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
})));
const detailJsons = await Promise.all(detailResponses.map((r) => r.ok ? r.json() : Promise.resolve({ success: false })));
const details = detailJsons
.filter((j) => j && j.success && j.data)
.map((j) => j.data);
// 以原顺序合并,缺失则回退基础信息
const merged = ids.map((id) => {
const d = details.find((x) => x.id === id);
const baseItem = (data.data || []).find((x) => x.id === id) || {};
return d ? d : baseItem;
});
setFollowingEvents(merged);
}
} else {
setFollowingEvents([]);
}
} else {
setFollowingEvents([]);
}
} catch (e) {
console.warn('加载关注事件失败:', e);
setFollowingEvents([]);
} finally {
setEventsLoading(false);
}
}, []);
// 从自选股移除
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
try {
const base = getApiBase();
const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, {
method: 'DELETE',
credentials: 'include'
});
const data = await resp.json().catch(() => ({}));
if (resp.ok && data && data.success !== false) {
setWatchlistQuotes((prev) => {
const normalize6 = (code) => {
const m = String(code || '').match(/(\d{6})/);
return m ? m[1] : String(code || '');
};
const target = normalize6(stockCode);
const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target);
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE));
setWatchlistPage((p) => Math.min(p, newMaxPage));
return updated;
});
toast({ title: '已从自选股移除', status: 'info', duration: 1500 });
} else {
toast({ title: '移除失败', status: 'error', duration: 2000 });
}
} catch (e) {
toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 });
}
}, [getApiBase, toast, WATCHLIST_PAGE_SIZE]);
// 取消关注事件
const handleUnfollowEvent = useCallback(async (eventId) => {
try {
const base = getApiBase();
const resp = await fetch(base + `/api/events/${eventId}/follow`, {
method: 'POST',
credentials: 'include'
});
const data = await resp.json().catch(() => ({}));
if (resp.ok && data && data.success !== false) {
setFollowingEvents((prev) => {
const updated = (prev || []).filter((x) => x.id !== eventId);
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / EVENTS_PAGE_SIZE));
setEventsPage((p) => Math.min(p, newMaxPage));
return updated;
});
toast({ title: '已取消关注该事件', status: 'info', duration: 1500 });
} else {
toast({ title: '操作失败', status: 'error', duration: 2000 });
}
} catch (e) {
toast({ title: '网络错误,操作失败', status: 'error', duration: 2000 });
}
}, [getApiBase, toast, EVENTS_PAGE_SIZE]);
// 检查用户资料完整性
const checkProfileCompleteness = useCallback(async () => {
if (!isAuthenticated || !user) return;
try {
const base = getApiBase();
const resp = await fetch(base + '/api/account/profile-completeness', {
credentials: 'include'
});
if (resp.ok) {
const data = await resp.json();
if (data.success) {
setProfileCompleteness(data.data);
// 只有微信用户且资料不完整时才显示提醒
setShowCompletenessAlert(data.data.needsAttention);
}
}
} catch (error) {
console.warn('检查资料完整性失败:', error);
}
}, [isAuthenticated, user, getApiBase]);
// 用户登录后检查资料完整性
React.useEffect(() => {
if (isAuthenticated && user) {
// 延迟检查,避免过于频繁
const timer = setTimeout(checkProfileCompleteness, 1000);
return () => clearTimeout(timer);
}
}, [isAuthenticated, user, checkProfileCompleteness]);
return (
<>
{/* 资料完整性提醒横幅 */}
{showCompletenessAlert && profileCompleteness && (
<Box
bg="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
color="white"
py={2}
px={4}
position="sticky"
top={0}
zIndex={1001}
>
<Container maxW="container.xl">
<HStack justify="space-between" align="center">
<HStack spacing={3}>
<Icon as={FiStar} />
<VStack spacing={0} align="start">
<Text fontSize="sm" fontWeight="bold">
完善资料享受更好服务
</Text>
<Text fontSize="xs" opacity={0.9}>
您还需要设置{profileCompleteness.missingItems.join('、')}
</Text>
</VStack>
<Text fontSize="xs" bg="whiteAlpha.300" px={2} py={1} borderRadius="full">
{profileCompleteness.completenessPercentage}% 完成
</Text>
</HStack>
<HStack spacing={2}>
<Button
size="sm"
colorScheme="whiteAlpha"
variant="ghost"
onClick={() => navigate('/home/settings')}
>
立即完善
</Button>
<IconButton
size="sm"
variant="ghost"
colorScheme="whiteAlpha"
icon={<Text>×</Text>}
onClick={() => setShowCompletenessAlert(false)}
aria-label="关闭提醒"
/>
</HStack>
</HStack>
</Container>
</Box>
)}
<Box
position="sticky"
top={showCompletenessAlert ? "60px" : 0}
zIndex={1000}
bg={navbarBg}
backdropFilter="blur(10px)"
borderBottom="1px"
borderColor={navbarBorder}
py={3}
>
<Container maxW="container.xl" px={4}>
<Flex justify="space-between" align="center">
{/* Logo - 价小前投研 */}
<HStack spacing={6}>
<Text
fontSize="xl"
fontWeight="bold"
color={brandText}
cursor="pointer"
_hover={{ color: brandHover }}
onClick={() => navigate('/home')}
style={{ minWidth: '140px' }}
>
价小前投研
</Text>
</HStack>
{/* 移动端菜单按钮 */}
{isMobile ? (
<IconButton
icon={<HamburgerIcon />}
variant="ghost"
onClick={onOpen}
aria-label="Open menu"
/>
) : <NavItems isAuthenticated={isAuthenticated} user={user} />}
{/* 右侧:日夜模式切换 + 登录/用户区 */}
<HStack spacing={4}>
<IconButton
aria-label="切换主题"
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
onClick={toggleColorMode}
variant="ghost"
size="sm"
/>
{/* 显示加载状态 */}
{isLoading ? (
<HStack spacing={2}>
<Spinner size="sm" />
<Text fontSize="sm" color="gray.500">检查登录状态...</Text>
</HStack>
) : isAuthenticated && user ? (
// 已登录状态 - 用户菜单 + 功能菜单排列
<HStack spacing={3}>
<Menu>
<MenuButton
as={Button}
bg="gray.800"
color="white"
size="sm"
borderRadius="full"
_hover={{ bg: 'gray.700' }}
leftIcon={
<Avatar
size="xs"
name={getDisplayName()}
src={user.avatar_url}
bg="blue.500"
/>
}
>
{getDisplayName()}
</MenuButton>
<MenuList>
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
<Text fontSize="xs" color="gray.500">{user.email}</Text>
{user.phone && (
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
)}
{user.has_wechat && (
<Badge size="sm" colorScheme="green" mt={1}>微信已绑定</Badge>
)}
</Box>
<MenuItem onClick={() => navigate('/home/profile')}>
👤 个人资料
</MenuItem>
<MenuItem onClick={() => navigate('/home/pages/account/subscription')}>
💎 订阅管理
</MenuItem>
<MenuItem onClick={() => navigate('/home/settings')}>
账户设置
</MenuItem>
<MenuItem onClick={() => navigate('/home/center')}>
🏠 个人中心
</MenuItem>
<MenuDivider />
<MenuItem onClick={handleLogout} color="red.500">
🚪 退出登录
</MenuItem>
</MenuList>
</Menu>
{/* 自选股 - 头像右侧 */}
<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={useColorModeValue('gray.600', 'gray.300')}>我的自选股</Text>
</Box>
{watchlistLoading ? (
<Box px={4} py={3}>
<HStack>
<Spinner size="sm" />
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.300')}>加载中...</Text>
</HStack>
</Box>
) : (
<>
{(!watchlistQuotes || watchlistQuotes.length === 0) ? (
<Box px={4} py={3}>
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.300')}>暂无自选股</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={useColorModeValue('gray.500', 'gray.400')}>{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>
<Button size="xs" variant="ghost" colorScheme="red" onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleRemoveFromWatchlist(item.stock_code); }}>取消</Button>
</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={useColorModeValue('gray.600', 'gray.400')}>{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>
{/* 关注的事件 - 头像右侧 */}
<Menu onOpen={loadFollowingEvents}>
<MenuButton
as={Button}
size="sm"
colorScheme="purple"
variant="solid"
borderRadius="full"
rightIcon={<ChevronDownIcon />}
leftIcon={<FiCalendar />}
>
自选事件
{followingEvents && followingEvents.length > 0 && (
<Badge ml={2} colorScheme="whiteAlpha">{followingEvents.length}</Badge>
)}
</MenuButton>
<MenuList minW="460px">
<Box px={4} py={2}>
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.300')}>我关注的事件</Text>
</Box>
{eventsLoading ? (
<Box px={4} py={3}>
<HStack>
<Spinner size="sm" />
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.300')}>加载中...</Text>
</HStack>
</Box>
) : (
<>
{(!followingEvents || followingEvents.length === 0) ? (
<Box px={4} py={3}>
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.300')}>暂未关注任何事件</Text>
</Box>
) : (
<VStack align="stretch" spacing={1} px={2} py={1}>
{followingEvents
.slice((eventsPage - 1) * EVENTS_PAGE_SIZE, eventsPage * EVENTS_PAGE_SIZE)
.map((ev) => (
<MenuItem key={ev.id} _hover={{ bg: 'gray.50' }} onClick={() => navigate(`/event-detail/${ev.id}`)}>
<HStack justify="space-between" w="100%">
<Box>
<Text fontSize="sm" fontWeight="medium" noOfLines={1}>{ev.title}</Text>
<HStack spacing={2}>
{ev.event_type && (
<Badge colorScheme="blue" fontSize="xs">{ev.event_type}</Badge>
)}
{ev.start_time && (
<Text fontSize="xs" color={useColorModeValue('gray.500', 'gray.400')}>{new Date(ev.start_time).toLocaleString('zh-CN')}</Text>
)}
</HStack>
</Box>
<HStack>
{typeof ev.related_avg_chg === 'number' && (
<Badge colorScheme={ev.related_avg_chg > 0 ? 'red' : (ev.related_avg_chg < 0 ? 'green' : 'gray')} fontSize="xs">日均 {ev.related_avg_chg > 0 ? '+' : ''}{ev.related_avg_chg.toFixed(2)}%</Badge>
)}
{typeof ev.related_week_chg === 'number' && (
<Badge colorScheme={ev.related_week_chg > 0 ? 'red' : (ev.related_week_chg < 0 ? 'green' : 'gray')} fontSize="xs">周涨 {ev.related_week_chg > 0 ? '+' : ''}{ev.related_week_chg.toFixed(2)}%</Badge>
)}
<Button size="xs" variant="ghost" colorScheme="red" onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleUnfollowEvent(ev.id); }}>取消</Button>
</HStack>
</HStack>
</MenuItem>
))}
</VStack>
)}
<MenuDivider />
<HStack justify="space-between" px={3} py={2}>
<HStack>
<Button size="xs" variant="outline" onClick={() => setEventsPage((p) => Math.max(1, p - 1))} isDisabled={eventsPage <= 1}>上一页</Button>
<Text fontSize="xs" color={useColorModeValue('gray.600', 'gray.400')}>{eventsPage} / {Math.max(1, Math.ceil((followingEvents?.length || 0) / EVENTS_PAGE_SIZE))}</Text>
<Button size="xs" variant="outline" onClick={() => setEventsPage((p) => Math.min(Math.ceil((followingEvents?.length || 0) / EVENTS_PAGE_SIZE) || 1, p + 1))} isDisabled={eventsPage >= Math.ceil((followingEvents?.length || 0) / EVENTS_PAGE_SIZE)}>下一页</Button>
</HStack>
<HStack>
<Button size="xs" variant="ghost" onClick={loadFollowingEvents}>刷新</Button>
<Button size="xs" colorScheme="purple" variant="ghost" onClick={() => navigate('/home/center')}>前往个人中心</Button>
</HStack>
</HStack>
</>
)}
</MenuList>
</Menu>
</HStack>
) : (
// 未登录状态
<Button
colorScheme="blue"
variant="solid"
size="sm"
borderRadius="full"
onClick={handleLoginClick}
_hover={{
transform: "translateY(-1px)",
boxShadow: "md"
}}
>
登录 / 注册
</Button>
)}
</HStack>
</Flex>
</Container>
{/* 移动端抽屉菜单 */}
<Drawer isOpen={isOpen} placement="right" onClose={onClose}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>
<HStack>
<Text>菜单</Text>
{isAuthenticated && user && (
<Badge colorScheme="green" ml={2}>已登录</Badge>
)}
</HStack>
</DrawerHeader>
<DrawerBody>
<VStack spacing={4} align="stretch">
{/* 移动端:日夜模式切换 */}
<Button
leftIcon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
variant="ghost"
justifyContent="flex-start"
onClick={toggleColorMode}
size="sm"
>
切换到{colorMode === 'light' ? '深色' : '浅色'}模式
</Button>
{/* 移动端用户信息 */}
{isAuthenticated && user && (
<>
<Box p={3} bg={useColorModeValue('gray.50', 'whiteAlpha.100')} borderRadius="md">
<HStack>
<Avatar
size="sm"
name={getDisplayName()}
src={user.avatar_url}
bg="blue.500"
/>
<Box>
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
<Text fontSize="xs" color={useColorModeValue('gray.500', 'gray.300')}>{user.email}</Text>
</Box>
</HStack>
</Box>
<Divider />
</>
)}
{/* 首页链接 */}
<Link
onClick={() => {
navigate('/home');
onClose();
}}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
color="blue.500"
fontWeight="bold"
>
<Text fontSize="md">🏠 首页</Text>
</Link>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>高频跟踪</Text>
<VStack spacing={2} align="stretch">
<Link
onClick={() => {
navigate('/community');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<HStack justify="space-between">
<Text fontSize="sm">新闻催化分析</Text>
<HStack spacing={1}>
<Badge size="xs" colorScheme="green">HOT</Badge>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</HStack>
</Link>
<Link
onClick={() => {
navigate('/concepts');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<HStack justify="space-between">
<Text fontSize="sm">概念中心</Text>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</Link>
</VStack>
</Box>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>行情复盘</Text>
<VStack spacing={2} align="stretch">
<Link
onClick={() => {
navigate('/limit-analyse');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<HStack justify="space-between">
<Text fontSize="sm">涨停分析</Text>
<Badge size="xs" colorScheme="blue">FREE</Badge>
</HStack>
</Link>
<Link
onClick={() => {
navigate('/stocks');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
<HStack justify="space-between">
<Text fontSize="sm">个股中心</Text>
<Badge size="xs" colorScheme="green">HOT</Badge>
</HStack>
</Link>
<Link
href="/trading-simulation"
isExternal
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
>
<HStack justify="space之间">
<Text fontSize="sm">模拟盘</Text>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</Link>
</VStack>
</Box>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>AGENT社群</Text>
<VStack spacing={2} align="stretch">
<Link
py={1}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">今日热议</Text>
</Link>
<Link
py={1}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">个股社区</Text>
</Link>
</VStack>
</Box>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>联系我们</Text>
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.300')}>敬请期待</Text>
</Box>
{/* 移动端登录/登出按钮 */}
<Divider />
{isAuthenticated && user ? (
<Button
colorScheme="red"
variant="outline"
size="sm"
onClick={() => {
handleLogout();
onClose();
}}
>
🚪 退出登录
</Button>
) : (
<Button
colorScheme="blue"
size="sm"
onClick={() => {
handleLoginClick();
onClose();
}}
>
🔐 登录 / 注册
</Button>
)}
</VStack>
</DrawerBody>
</DrawerContent>
</Drawer>
</Box>
</>
);
}