import React, { useState, useEffect } from 'react';
import {
Box,
Container,
VStack,
HStack,
Heading,
Text,
Badge,
useToast,
Skeleton,
IconButton,
Flex,
useColorModeValue,
SimpleGrid,
Tooltip,
Card,
CardBody,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
Alert,
AlertIcon,
} from '@chakra-ui/react';
import {
RepeatIcon,
ChevronUpIcon,
} from '@chakra-ui/icons';
// 导入拆分的组件
// 注意:在实际使用中,这些组件应该被拆分到独立的文件中
// 这里为了演示,我们假设它们已经被正确导出
// API配置
const API_URL = process.env.NODE_ENV === 'production' ? '/report-api' : 'http://111.198.58.126:5001';
// 导入的组件(实际使用时应该从独立文件导入)
// 恢复使用本页自带的轻量日历
import EnhancedCalendar from './components/EnhancedCalendar';
import SectorDetails from './components/SectorDetails';
import { DataAnalysis, StockDetailModal } from './components/DataVisualizationComponents';
import { AdvancedSearch, SearchResultsModal } from './components/SearchComponents';
// 导航栏已由 MainLayout 提供,无需在此导入
// 导入高位股统计组件
import HighPositionStocks from './components/HighPositionStocks';
import { logger } from '../../utils/logger';
import { useLimitAnalyseEvents } from './hooks/useLimitAnalyseEvents';
// 主组件
export default function LimitAnalyse() {
const [selectedDate, setSelectedDate] = useState(null);
const [dateStr, setDateStr] = useState('');
const [loading, setLoading] = useState(false);
const [dailyData, setDailyData] = useState(null);
const [availableDates, setAvailableDates] = useState([]);
const [wordCloudData, setWordCloudData] = useState([]);
const [searchResults, setSearchResults] = useState(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [selectedStock, setSelectedStock] = useState(null);
const [isStockDetailOpen, setIsStockDetailOpen] = useState(false);
const toast = useToast();
// 🎯 PostHog 事件追踪
const {
trackDateSelected,
trackDailyStatsViewed,
trackSectorToggled,
trackSectorClicked,
trackLimitStockClicked,
trackSearchInitiated,
trackSearchResultClicked,
trackHighPositionStocksViewed,
trackSectorAnalysisViewed,
trackDataRefreshed,
trackStockDetailViewed,
} = useLimitAnalyseEvents();
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
const accentColor = useColorModeValue('blue.500', 'blue.300');
// 获取可用日期
useEffect(() => {
fetchAvailableDates();
}, []);
// 初始进入展示骨架屏,直到选中日期的数据加载完成
useEffect(() => {
setLoading(true);
}, []);
// 根据可用日期加载最近一个有数据的日期
useEffect(() => {
if (availableDates && availableDates.length > 0) {
// 选择日期字符串最大的那一天(格式为 YYYYMMDD)
const latest = availableDates.reduce((max, cur) =>
(!max || (cur.date && cur.date > max)) ? cur.date : max
, null);
if (latest) {
setDateStr(latest);
const year = parseInt(latest.slice(0, 4), 10);
const month = parseInt(latest.slice(4, 6), 10) - 1;
const day = parseInt(latest.slice(6, 8), 10);
setSelectedDate(new Date(year, month, day));
fetchDailyAnalysis(latest);
}
} else {
// 如果暂无可用日期,回退到今日,避免页面长时间空白
const today = new Date();
const dateString = formatDateStr(today);
setDateStr(dateString);
setSelectedDate(today);
fetchDailyAnalysis(dateString);
}
}, [availableDates]);
// API调用函数
const fetchAvailableDates = async () => {
try {
const response = await fetch(`${API_URL}/api/v1/dates/available`);
const data = await response.json();
if (data.success) {
setAvailableDates(data.events);
logger.debug('LimitAnalyse', '可用日期加载成功', {
count: data.events?.length || 0
});
}
} catch (error) {
logger.error('LimitAnalyse', 'fetchAvailableDates', error);
}
};
const fetchDailyAnalysis = async (date) => {
setLoading(true);
try {
const response = await fetch(`${API_URL}/api/v1/analysis/daily/${date}`);
const data = await response.json();
if (data.success) {
setDailyData(data.data);
// 🎯 追踪每日统计数据查看
trackDailyStatsViewed(data.data, date);
// 获取词云数据
fetchWordCloudData(date);
logger.debug('LimitAnalyse', '每日分析数据加载成功', {
date,
totalStocks: data.data?.total_stocks || 0
});
// ❌ 移除数据加载成功 toast(非关键操作)
}
} catch (error) {
logger.error('LimitAnalyse', 'fetchDailyAnalysis', error, { date });
// ❌ 移除数据加载失败 toast(非关键操作)
} finally {
setLoading(false);
}
};
const fetchWordCloudData = async (date) => {
try {
const response = await fetch(`${API_URL}/api/v1/analysis/wordcloud/${date}`);
const data = await response.json();
if (data.success) {
setWordCloudData(data.data);
logger.debug('LimitAnalyse', '词云数据加载成功', {
date,
count: data.data?.length || 0
});
}
} catch (error) {
logger.error('LimitAnalyse', 'fetchWordCloudData', error, { date });
}
};
// 格式化日期
const formatDateStr = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
};
// 处理日期选择
const handleDateChange = (date) => {
const previousDateStr = dateStr;
setSelectedDate(date);
const dateString = formatDateStr(date);
setDateStr(dateString);
// 🎯 追踪日期选择
trackDateSelected(dateString, previousDateStr);
fetchDailyAnalysis(dateString);
};
// 处理搜索
const handleSearch = async (searchParams) => {
// 🎯 追踪搜索开始
trackSearchInitiated(
searchParams.query,
searchParams.type || 'all',
searchParams.mode || 'hybrid'
);
setLoading(true);
try {
const response = await fetch(`${API_URL}/api/v1/stocks/search/hybrid`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(searchParams),
});
const data = await response.json();
if (data.success) {
setSearchResults(data.data);
setIsSearchOpen(true);
logger.info('LimitAnalyse', '搜索完成', {
resultCount: data.data?.total || 0,
searchParams
});
toast({
title: '搜索完成',
description: `找到 ${data.data.total} 只相关股票`,
status: 'success',
duration: 3000,
});
}
} catch (error) {
logger.error('LimitAnalyse', 'handleSearch', error, { searchParams });
toast({
title: '搜索失败',
description: '请稍后重试',
status: 'error',
duration: 3000,
});
} finally {
setLoading(false);
}
};
// 处理股票点击
const handleStockClick = (stock) => {
setSelectedStock(stock);
setIsStockDetailOpen(true);
// 🎯 追踪股票详情查看
trackStockDetailViewed(stock.scode, stock.sname, 'sector_details');
};
// 关闭股票详情弹窗
const handleCloseStockDetail = () => {
setIsStockDetailOpen(false);
setSelectedStock(null);
};
// 处理板块数据排序
const getSortedSectorData = () => {
if (!dailyData?.sector_data) return [];
const sectors = Object.entries(dailyData.sector_data);
const announcement = sectors.find(([name]) => name === '公告');
const others = sectors.filter(([name]) => name !== '公告' && name !== '其他');
const other = sectors.find(([name]) => name === '其他');
// 按数量排序
others.sort((a, b) => b[1].count - a[1].count);
// 组合:公告在最前,其他在最后
let result = [];
if (announcement) result.push(announcement);
result = result.concat(others);
if (other) result.push(other);
return result;
};
// 渲染统计卡片
const StatsCards = () => (
今日涨停
{dailyData?.total_stocks || 0}
较昨日 +23%
最热板块
{dailyData?.summary?.top_sector || '-'}
{dailyData?.summary?.top_sector_count || 0} 只
公告涨停
{dailyData?.summary?.announcement_stocks || 0}
重大利好
早盘强势
{dailyData?.summary?.zt_time_distribution?.morning || 0}
开盘涨停
);
const formatDisplayDate = (date) => {
if (!date) return '';
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}年${month}月${day}日`;
};
const getSelectedDateCount = () => {
if (!selectedDate || !availableDates?.length) return null;
const date = formatDateStr(selectedDate);
const found = availableDates.find(d => d.date === date);
return found ? found.count : null;
};
return (
{/* 导航栏已由 MainLayout 提供 */}
{/* 顶部Header */}
{/* 左侧:标题置顶,注释与图例贴底 */}
AI驱动
实时更新
涨停板块分析平台
以大模型辅助整理海量信息,结合领域知识图谱与分析师复核,呈现涨停板块关键线索
{selectedDate ? `当前选择:${formatDisplayDate(selectedDate)}` : '当前选择:--'}
{getSelectedDateCount() != null ? ` - ${getSelectedDateCount()}只涨停` : ''}
涨停数量图例
少量 (≤50只)
中等 (51-80只)
大量 (>80只)
{/* 右侧:半屏日历 */}
{/* 主内容区 */}
{/* 统计卡片 */}
{loading ? (
{[...Array(4)].map((_, i) => (
))}
) : (
)}
{/* 高级搜索 */}
{/* 板块详情 - 核心内容 */}
{loading ? (
) : (
)}
{/* 高位股统计 */}
{/* 数据分析 */}
{loading ? (
) : (
)}
{/* 弹窗 */}
setIsSearchOpen(false)}
searchResults={searchResults}
onStockClick={() => {}}
/>
{/* 股票详情弹窗 */}
{/* 浮动按钮 */}
}
colorScheme="blue"
size="lg"
borderRadius="full"
boxShadow="2xl"
onClick={() => fetchDailyAnalysis(dateStr)}
isLoading={loading}
/>
}
colorScheme="gray"
size="lg"
borderRadius="full"
boxShadow="2xl"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
/>
);
}