Compare commits
8 Commits
5236976307
...
f05daa3a78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f05daa3a78 | ||
|
|
2461ce81c9 | ||
|
|
85d505cd53 | ||
|
|
1886c54e0f | ||
|
|
6829f687ee | ||
|
|
47f84c5eff | ||
|
|
a0d1790469 | ||
|
|
0364b3a927 |
@@ -12,7 +12,8 @@ import {
|
||||
Text,
|
||||
Flex,
|
||||
Badge,
|
||||
useColorModeValue
|
||||
useColorModeValue,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
@@ -36,6 +37,12 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
// 🎯 初始化导航埋点Hook
|
||||
const navEvents = useNavigationEvents({ component: 'top_nav' });
|
||||
|
||||
// 🎯 为每个菜单创建独立的 useDisclosure Hook
|
||||
const { isOpen: isHighFreqOpen, onOpen: onHighFreqOpen, onClose: onHighFreqClose } = useDisclosure();
|
||||
const { isOpen: isMarketReviewOpen, onOpen: onMarketReviewOpen, onClose: onMarketReviewClose } = useDisclosure();
|
||||
const { isOpen: isAgentCommunityOpen, onOpen: onAgentCommunityOpen, onClose: onAgentCommunityClose } = useDisclosure();
|
||||
const { isOpen: isContactUsOpen, onOpen: onContactUsOpen, onClose: onContactUsClose } = useDisclosure();
|
||||
|
||||
// 辅助函数:判断导航项是否激活
|
||||
const isActive = useCallback((paths) => {
|
||||
return paths.some(path => location.pathname.includes(path));
|
||||
@@ -46,7 +53,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
return (
|
||||
<HStack spacing={8}>
|
||||
{/* 高频跟踪 */}
|
||||
<Menu>
|
||||
<Menu isOpen={isHighFreqOpen} onClose={onHighFreqClose}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
@@ -57,10 +64,12 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'}
|
||||
borderColor="blue.600"
|
||||
_hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }}
|
||||
onMouseEnter={onHighFreqOpen}
|
||||
onMouseLeave={onHighFreqClose}
|
||||
>
|
||||
高频跟踪
|
||||
</MenuButton>
|
||||
<MenuList minW="260px" p={2}>
|
||||
<MenuList minW="260px" p={2} onMouseEnter={onHighFreqOpen} onMouseLeave={onHighFreqClose}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
// 🎯 追踪菜单项点击
|
||||
@@ -102,7 +111,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
</Menu>
|
||||
|
||||
{/* 行情复盘 */}
|
||||
<Menu>
|
||||
<Menu isOpen={isMarketReviewOpen} onClose={onMarketReviewClose}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
@@ -113,10 +122,12 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
borderBottom={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? '2px solid' : 'none'}
|
||||
borderColor="blue.600"
|
||||
_hover={{ bg: isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.100' : 'gray.50' }}
|
||||
onMouseEnter={onMarketReviewOpen}
|
||||
onMouseLeave={onMarketReviewClose}
|
||||
>
|
||||
行情复盘
|
||||
</MenuButton>
|
||||
<MenuList minW="260px" p={2}>
|
||||
<MenuList minW="260px" p={2} onMouseEnter={onMarketReviewOpen} onMouseLeave={onMarketReviewClose}>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/limit-analyse')}
|
||||
borderRadius="md"
|
||||
@@ -160,11 +171,17 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
</Menu>
|
||||
|
||||
{/* AGENT社群 */}
|
||||
<Menu>
|
||||
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
|
||||
<Menu isOpen={isAgentCommunityOpen} onClose={onAgentCommunityClose}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
onMouseEnter={onAgentCommunityOpen}
|
||||
onMouseLeave={onAgentCommunityClose}
|
||||
>
|
||||
AGENT社群
|
||||
</MenuButton>
|
||||
<MenuList minW="300px" p={4}>
|
||||
<MenuList minW="300px" p={4} onMouseEnter={onAgentCommunityOpen} onMouseLeave={onAgentCommunityClose}>
|
||||
<MenuItem
|
||||
isDisabled
|
||||
cursor="not-allowed"
|
||||
@@ -183,11 +200,17 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
</Menu>
|
||||
|
||||
{/* 联系我们 */}
|
||||
<Menu>
|
||||
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
|
||||
<Menu isOpen={isContactUsOpen} onClose={onContactUsClose}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
onMouseEnter={onContactUsOpen}
|
||||
onMouseLeave={onContactUsClose}
|
||||
>
|
||||
联系我们
|
||||
</MenuButton>
|
||||
<MenuList minW="260px" p={4}>
|
||||
<MenuList minW="260px" p={4} onMouseEnter={onContactUsOpen} onMouseLeave={onContactUsClose}>
|
||||
<Text fontSize="sm" color={contactTextColor}>敬请期待</Text>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
Text,
|
||||
Flex,
|
||||
HStack,
|
||||
Badge
|
||||
Badge,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
@@ -29,6 +30,9 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// 🎯 为"更多"菜单创建 useDisclosure Hook
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// 辅助函数:判断导航项是否激活
|
||||
const isActive = useCallback((paths) => {
|
||||
return paths.some(path => location.pathname.includes(path));
|
||||
@@ -37,16 +41,18 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||
if (!isAuthenticated || !user) return null;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu isOpen={isOpen} onClose={onClose}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
fontWeight="medium"
|
||||
onMouseEnter={onOpen}
|
||||
onMouseLeave={onClose}
|
||||
>
|
||||
更多
|
||||
</MenuButton>
|
||||
<MenuList minW="300px" p={2}>
|
||||
<MenuList minW="300px" p={2} onMouseEnter={onOpen} onMouseLeave={onClose}>
|
||||
{/* 高频跟踪组 */}
|
||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">高频跟踪</Text>
|
||||
<MenuItem
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
Box,
|
||||
Text,
|
||||
Badge,
|
||||
useColorModeValue
|
||||
useColorModeValue,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import { FiHome, FiUser, FiSettings, FiLogOut } from 'react-icons/fi';
|
||||
@@ -31,6 +32,9 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
||||
const navigate = useNavigate();
|
||||
const hoverBg = useColorModeValue('gray.100', 'gray.700');
|
||||
|
||||
// 🎯 为个人中心菜单创建 useDisclosure Hook
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// 获取显示名称
|
||||
const getDisplayName = () => {
|
||||
if (user.nickname) return user.nickname;
|
||||
@@ -41,17 +45,19 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu isOpen={isOpen} onClose={onClose}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
_hover={{ bg: hoverBg }}
|
||||
onMouseEnter={onOpen}
|
||||
onMouseLeave={onClose}
|
||||
>
|
||||
个人中心
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuList onMouseEnter={onOpen} onMouseLeave={onClose}>
|
||||
{/* 用户信息区 */}
|
||||
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||
|
||||
@@ -346,6 +346,11 @@ const NotificationItem = React.memo(({ notification, onClose, isNewest = false }
|
||||
`${typeConfig.colorScheme}.200`,
|
||||
`${typeConfig.colorScheme}.700`
|
||||
);
|
||||
// 最新通知的 borderTopColor(避免在条件语句中调用 Hook)
|
||||
const newestBorderTopColor = useColorModeValue(
|
||||
`${typeConfig.colorScheme}.100`,
|
||||
`${typeConfig.colorScheme}.700`
|
||||
);
|
||||
|
||||
// 使用 useMemo 缓存颜色对象(避免不必要的重新创建)
|
||||
const colors = useMemo(() => ({
|
||||
@@ -357,7 +362,8 @@ const NotificationItem = React.memo(({ notification, onClose, isNewest = false }
|
||||
metaText: metaTextColor,
|
||||
hoverBg: hoverBgColor,
|
||||
closeButtonHoverBg: closeButtonHoverBgColor,
|
||||
}), [priorityBgColor, borderColor, iconColor, textColor, subTextColor, metaTextColor, hoverBgColor, closeButtonHoverBgColor]);
|
||||
newestBorderTop: newestBorderTopColor,
|
||||
}), [priorityBgColor, borderColor, iconColor, textColor, subTextColor, metaTextColor, hoverBgColor, closeButtonHoverBgColor, newestBorderTopColor]);
|
||||
|
||||
// 点击处理(只有真正可点击时才执行)- 使用 useCallback 优化
|
||||
const handleClick = useCallback(() => {
|
||||
@@ -430,7 +436,7 @@ const NotificationItem = React.memo(({ notification, onClose, isNewest = false }
|
||||
borderRight: '1px solid',
|
||||
borderRightColor: colors.border,
|
||||
borderTop: '1px solid',
|
||||
borderTopColor: useColorModeValue(`${typeConfig.colorScheme}.100`, `${typeConfig.colorScheme}.700`),
|
||||
borderTopColor: colors.newestBorderTop,
|
||||
})}
|
||||
>
|
||||
{/* 头部区域:标题 + 可选标识 */}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
// src/lib/posthog.js
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
// 初始化状态管理(防止重复初始化)
|
||||
let isInitializing = false;
|
||||
let isInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize PostHog SDK
|
||||
* Should be called once when the app starts
|
||||
*/
|
||||
export const initPostHog = () => {
|
||||
// 防止重复初始化
|
||||
if (isInitializing || isInitialized) {
|
||||
console.log('📊 PostHog 已初始化或正在初始化中,跳过重复调用');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only run in browser environment
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
@@ -17,6 +27,8 @@ export const initPostHog = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
isInitializing = true;
|
||||
|
||||
try {
|
||||
posthog.init(apiKey, {
|
||||
api_host: apiHost,
|
||||
@@ -85,9 +97,17 @@ export const initPostHog = () => {
|
||||
},
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
console.log('📊 PostHog Analytics initialized');
|
||||
} catch (error) {
|
||||
// 忽略 AbortError(通常由热重载或快速导航引起)
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('⚠️ PostHog 初始化请求被中断(可能是热重载),这是正常的');
|
||||
return;
|
||||
}
|
||||
console.error('❌ PostHog initialization failed:', error);
|
||||
} finally {
|
||||
isInitializing = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,8 +7,18 @@ import { handlers } from './handlers';
|
||||
// 创建 Service Worker 实例
|
||||
export const worker = setupWorker(...handlers);
|
||||
|
||||
// 启动状态管理(防止重复启动)
|
||||
let isStarting = false;
|
||||
let isStarted = false;
|
||||
|
||||
// 启动 Mock Service Worker
|
||||
export async function startMockServiceWorker() {
|
||||
// 防止重复启动
|
||||
if (isStarting || isStarted) {
|
||||
console.log('[MSW] Mock Service Worker 已启动或正在启动中,跳过重复调用');
|
||||
return;
|
||||
}
|
||||
|
||||
// 只在开发环境且 REACT_APP_ENABLE_MOCK=true 时启动
|
||||
const shouldEnableMock = process.env.REACT_APP_ENABLE_MOCK === 'true';
|
||||
|
||||
@@ -17,6 +27,8 @@ export async function startMockServiceWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
isStarting = true;
|
||||
|
||||
try {
|
||||
await worker.start({
|
||||
// 🎯 智能穿透模式(关键配置)
|
||||
@@ -34,6 +46,7 @@ export async function startMockServiceWorker() {
|
||||
quiet: false,
|
||||
});
|
||||
|
||||
isStarted = true;
|
||||
console.log(
|
||||
'%c[MSW] Mock Service Worker 已启动 🎭',
|
||||
'color: #4CAF50; font-weight: bold; font-size: 14px;'
|
||||
@@ -48,12 +61,20 @@ export async function startMockServiceWorker() {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[MSW] 启动失败:', error);
|
||||
} finally {
|
||||
isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止 Mock Service Worker
|
||||
export function stopMockServiceWorker() {
|
||||
if (!isStarted) {
|
||||
console.log('[MSW] Mock Service Worker 未启动,无需停止');
|
||||
return;
|
||||
}
|
||||
|
||||
worker.stop();
|
||||
isStarted = false;
|
||||
console.log('[MSW] Mock Service Worker 已停止');
|
||||
}
|
||||
|
||||
|
||||
@@ -133,9 +133,12 @@ const InvestmentCalendar = () => {
|
||||
loadEventCounts(currentMonth);
|
||||
}, [currentMonth, loadEventCounts]);
|
||||
|
||||
// 自定义日期单元格渲染
|
||||
const dateCellRender = (value) => {
|
||||
const dateStr = value.format('YYYY-MM-DD');
|
||||
// 自定义日期单元格渲染(Ant Design 5.x API)
|
||||
const cellRender = (current, info) => {
|
||||
// 只处理日期单元格,月份单元格返回默认
|
||||
if (info.type !== 'date') return info.originNode;
|
||||
|
||||
const dateStr = current.format('YYYY-MM-DD');
|
||||
const dayEvents = eventCounts.find(item => item.date === dateStr);
|
||||
|
||||
if (dayEvents && dayEvents.count > 0) {
|
||||
@@ -681,7 +684,7 @@ const InvestmentCalendar = () => {
|
||||
>
|
||||
<Calendar
|
||||
fullscreen={false}
|
||||
dateCellRender={dateCellRender}
|
||||
cellRender={cellRender}
|
||||
onSelect={handleDateSelect}
|
||||
onPanelChange={(date) => setCurrentMonth(date)}
|
||||
/>
|
||||
@@ -695,11 +698,11 @@ const InvestmentCalendar = () => {
|
||||
<span>{selectedDate?.format('YYYY年MM月DD日')} 投资事件</span>
|
||||
</Space>
|
||||
}
|
||||
visible={modalVisible}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={1200}
|
||||
footer={null}
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
styles={{ body: { padding: '24px' } }}
|
||||
zIndex={1500}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
@@ -734,7 +737,7 @@ const InvestmentCalendar = () => {
|
||||
placement="right"
|
||||
width={600}
|
||||
onClose={() => setDetailDrawerVisible(false)}
|
||||
visible={detailDrawerVisible}
|
||||
open={detailDrawerVisible}
|
||||
zIndex={1500}
|
||||
>
|
||||
{selectedDetail?.content?.type === 'citation' ? (
|
||||
@@ -760,7 +763,7 @@ const InvestmentCalendar = () => {
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
visible={stockModalVisible}
|
||||
open={stockModalVisible}
|
||||
onCancel={() => {
|
||||
setStockModalVisible(false);
|
||||
setExpandedReasons({}); // 清理展开状态
|
||||
|
||||
@@ -244,8 +244,8 @@ const StockOverview = () => {
|
||||
const newStats = {
|
||||
...data.summary,
|
||||
// 保留之前从 heatmap 接口获取的上涨/下跌家数
|
||||
rising_count: prevStats?.rising_count,
|
||||
falling_count: prevStats?.falling_count,
|
||||
rising_count: marketStats?.rising_count,
|
||||
falling_count: marketStats?.falling_count,
|
||||
date: data.trade_date
|
||||
};
|
||||
setMarketStats(newStats);
|
||||
|
||||
@@ -92,6 +92,7 @@ export default function TradingSimulation() {
|
||||
const xAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const yAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
|
||||
const gridBorderColor = useColorModeValue('#E2E8F0', '#4A5568');
|
||||
const contentTextColor = useColorModeValue('gray.700', 'white');
|
||||
|
||||
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
|
||||
useEffect(() => {
|
||||
@@ -346,7 +347,7 @@ export default function TradingSimulation() {
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 账户概览统计 */}
|
||||
<Box>
|
||||
<Heading size="lg" mb={4} color={useColorModeValue('gray.700', 'white')}>
|
||||
<Heading size="lg" mb={4} color={contentTextColor}>
|
||||
📊 账户统计分析
|
||||
</Heading>
|
||||
<AccountOverview account={account} tradingEvents={tradingEvents} />
|
||||
@@ -357,7 +358,7 @@ export default function TradingSimulation() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="lg" fontWeight="bold" color={useColorModeValue('gray.700', 'white')}>
|
||||
<Text fontSize="lg" fontWeight="bold" color={contentTextColor}>
|
||||
📈 资产走势分析
|
||||
</Text>
|
||||
<Badge colorScheme="purple" variant="outline">
|
||||
|
||||
Reference in New Issue
Block a user