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';
import { useAuthModal } from '../../contexts/AuthModalContext';
/** 桌面端导航 - 完全按照原网站
* @TODO 添加逻辑 不展示导航case
* 1.未登陆状态 && 是首页
* 2. !isMobile
*/
const NavItems = ({ isAuthenticated, user }) => {
const navigate = useNavigate();
if (isAuthenticated && user) {
return (
{false && isAuthenticated && (
)}
{false && isAuthenticated && (
)}
)
} 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 { openAuthModal } = useAuthModal();
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);
}
};
// 检查是否为禁用的链接(没有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 && (
完善资料,享受更好服务
您还需要设置:{profileCompleteness.missingItems.join('、')}
{profileCompleteness.completenessPercentage}% 完成
×}
onClick={() => setShowCompletenessAlert(false)}
aria-label="关闭提醒"
/>
)}
{/* Logo - 价小前投研 */}
navigate('/home')}
style={{ minWidth: '140px' }}
>
价小前投研
{/* 移动端菜单按钮 */}
{isMobile ? (
}
variant="ghost"
onClick={onOpen}
aria-label="Open menu"
/>
) : }
{/* 右侧:日夜模式切换 + 登录/用户区 */}
: }
onClick={toggleColorMode}
variant="ghost"
size="sm"
/>
{/* 显示加载状态 */}
{isLoading ? (
检查登录状态...
) : isAuthenticated && user ? (
// 已登录状态 - 用户菜单 + 功能菜单排列
{/* 自选股 - 头像右侧 */}
{/* 关注的事件 - 头像右侧 */}
) : (
// 未登录状态 - 单一按钮
)}
{/* 移动端抽屉菜单 */}
菜单
{isAuthenticated && user && (
已登录
)}
{/* 移动端:日夜模式切换 */}
: }
variant="ghost"
justifyContent="flex-start"
onClick={toggleColorMode}
size="sm"
>
切换到{colorMode === 'light' ? '深色' : '浅色'}模式
{/* 移动端用户信息 */}
{isAuthenticated && user && (
<>
{getDisplayName()}
{user.email}
>
)}
{/* 首页链接 */}
{
navigate('/home');
onClose();
}}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
color="blue.500"
fontWeight="bold"
>
🏠 首页
高频跟踪
{
navigate('/community');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
新闻催化分析
HOT
NEW
{
navigate('/concepts');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
概念中心
NEW
行情复盘
{
navigate('/limit-analyse');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
涨停分析
FREE
{
navigate('/stocks');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
>
个股中心
HOT
模拟盘
NEW
AGENT社群
今日热议
个股社区
联系我们
敬请期待
{/* 移动端登录/登出按钮 */}
{isAuthenticated && user ? (
) : (
)}
>
);
}