- 整合市场全景、板块异动、高位股统计模块
- 状态提升实现板块点击联动(selectedSector)
- 更新 ztStaticService 静态数据服务:
- 添加缓存机制(dates 5分钟、daily 30分钟)
- 转换 stock_codes 为完整 stocks 对象
- 支持 sector_relations 板块关联数据
- 更新 Mock handlers:
- 完善 dates.json / daily/{date}.json 静态路径
- 添加 sector_relations 网络图数据生成
- 支持 chart_data 饼图数据结构
311 lines
11 KiB
JavaScript
Executable File
311 lines
11 KiB
JavaScript
Executable File
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
VStack,
|
||
useToast,
|
||
Skeleton,
|
||
IconButton,
|
||
useColorModeValue,
|
||
Tooltip,
|
||
} from '@chakra-ui/react';
|
||
import { RefreshCw, ChevronUp } from 'lucide-react';
|
||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||
|
||
// 使用静态数据服务(从 /data/zt/ 读取 JSON 文件)
|
||
import ztStaticService from '../../services/ztStaticService';
|
||
|
||
// 导入的组件
|
||
import LimitUpEmotionCycle from './components/LimitUpEmotionCycle';
|
||
import { SearchResultsModal } from './components/SearchComponents';
|
||
|
||
// 导入市场全景模块
|
||
import MarketPanorama from './components/MarketPanorama';
|
||
import { logger } from '../../utils/logger';
|
||
import { useLimitAnalyseEvents } from './hooks/useLimitAnalyseEvents';
|
||
|
||
// 玻璃拟态样式(保留供将来使用)
|
||
// eslint-disable-next-line no-unused-vars
|
||
const glassStyle = {
|
||
bg: 'rgba(15, 15, 22, 0.9)',
|
||
backdropFilter: `${GLASS_BLUR.lg} saturate(180%)`,
|
||
border: '1px solid rgba(212, 175, 55, 0.15)',
|
||
borderRadius: '20px',
|
||
};
|
||
|
||
// 主组件
|
||
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(null); // null 表示未加载,[] 表示加载完成但无数据
|
||
const [wordCloudData, setWordCloudData] = useState([]);
|
||
const [searchResults, setSearchResults] = useState(null);
|
||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||
const [selectedSector, setSelectedSector] = useState(null);
|
||
|
||
const toast = useToast();
|
||
|
||
// 处理板块点击(从热力图联动到板块异动明细)
|
||
const handleSectorSelect = (sectorName) => {
|
||
setSelectedSector(sectorName);
|
||
};
|
||
|
||
// 🎯 PostHog 事件追踪
|
||
const {
|
||
trackDateSelected,
|
||
trackDailyStatsViewed,
|
||
trackSearchInitiated,
|
||
} = 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(() => {
|
||
// 等待日期列表加载完成(null 表示未加载)
|
||
if (availableDates === null) {
|
||
return;
|
||
}
|
||
|
||
if (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 {
|
||
// 日期列表为空,显示提示但不请求数据
|
||
setLoading(false);
|
||
logger.warn('LimitAnalyse', '暂无可用数据');
|
||
}
|
||
}, [availableDates]);
|
||
|
||
// 使用静态数据服务获取数据
|
||
const fetchAvailableDates = async () => {
|
||
try {
|
||
const data = await ztStaticService.fetchAvailableDates();
|
||
if (data.success) {
|
||
setAvailableDates(data.events || []);
|
||
logger.debug('LimitAnalyse', '可用日期加载成功(静态文件)', {
|
||
count: data.events?.length || 0
|
||
});
|
||
} else {
|
||
// 请求成功但返回失败,设置空数组
|
||
setAvailableDates([]);
|
||
logger.warn('LimitAnalyse', '日期列表返回失败', data.error);
|
||
}
|
||
} catch (error) {
|
||
// 请求失败,设置空数组避免一直 loading
|
||
setAvailableDates([]);
|
||
logger.error('LimitAnalyse', 'fetchAvailableDates', error);
|
||
}
|
||
};
|
||
|
||
const fetchDailyAnalysis = async (date) => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await ztStaticService.fetchDailyAnalysis(date);
|
||
if (data.success) {
|
||
setDailyData(data.data);
|
||
|
||
// 🎯 追踪每日统计数据查看
|
||
trackDailyStatsViewed(data.data, date);
|
||
|
||
// 词云数据已包含在分析数据中
|
||
setWordCloudData(data.data.word_freq_data || []);
|
||
|
||
logger.debug('LimitAnalyse', '每日分析数据加载成功(静态文件)', {
|
||
date,
|
||
totalStocks: data.data?.total_stocks || 0,
|
||
fromCache: data.from_cache
|
||
});
|
||
}
|
||
} catch (error) {
|
||
logger.error('LimitAnalyse', 'fetchDailyAnalysis', error, { date });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 格式化日期
|
||
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);
|
||
|
||
// 日期切换时清空板块选择
|
||
setSelectedSector(null);
|
||
|
||
// 🎯 追踪日期选择
|
||
trackDateSelected(dateString, previousDateStr);
|
||
|
||
fetchDailyAnalysis(dateString);
|
||
};
|
||
|
||
// 处理搜索(使用静态数据关键词搜索)
|
||
const handleSearch = async (searchParams) => {
|
||
// 🎯 追踪搜索开始
|
||
trackSearchInitiated(
|
||
searchParams.query,
|
||
searchParams.type || 'all',
|
||
searchParams.mode || 'keyword' // 静态模式只支持关键词搜索
|
||
);
|
||
|
||
setLoading(true);
|
||
try {
|
||
const data = await ztStaticService.searchStocks(searchParams);
|
||
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,
|
||
});
|
||
} else {
|
||
toast({
|
||
title: '搜索失败',
|
||
description: data.error || '请稍后重试',
|
||
status: 'error',
|
||
duration: 3000,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
logger.error('LimitAnalyse', 'handleSearch', error, { searchParams });
|
||
toast({
|
||
title: '搜索失败',
|
||
description: '请稍后重试',
|
||
status: 'error',
|
||
duration: 3000,
|
||
});
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 处理板块数据排序
|
||
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;
|
||
};
|
||
|
||
return (
|
||
<Box minH="100vh" bg={bgColor}>
|
||
{/* 导航栏已由 MainLayout 提供 */}
|
||
|
||
{/* ==================== 第一部分:宏观情绪定调 ==================== */}
|
||
{/* 涨停情绪周期 - 置顶,判断市场处于周期的什么阶段 */}
|
||
<Box mb={6}>
|
||
<LimitUpEmotionCycle
|
||
selectedDate={selectedDate}
|
||
onDateChange={handleDateChange}
|
||
availableDates={availableDates}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 主内容区 */}
|
||
<Box>
|
||
{/* 市场全景与板块分析模块 */}
|
||
{loading ? (
|
||
<Skeleton height="500px" borderRadius="xl" mb={6} />
|
||
) : (
|
||
<MarketPanorama
|
||
dailyData={dailyData}
|
||
wordCloudData={wordCloudData}
|
||
totalStocks={dailyData?.total_stocks || 0}
|
||
selectedSector={selectedSector}
|
||
onSectorSelect={handleSectorSelect}
|
||
sortedSectors={getSortedSectorData()}
|
||
dateStr={dateStr}
|
||
/>
|
||
)}
|
||
|
||
</Box>
|
||
|
||
{/* 弹窗 */}
|
||
<SearchResultsModal
|
||
isOpen={isSearchOpen}
|
||
onClose={() => setIsSearchOpen(false)}
|
||
searchResults={searchResults}
|
||
onStockClick={() => {}}
|
||
/>
|
||
|
||
{/* 浮动按钮 */}
|
||
<Box position="fixed" bottom={8} right={8} zIndex={1000}>
|
||
<VStack spacing={3}>
|
||
<Tooltip label="刷新数据" placement="left">
|
||
<IconButton
|
||
icon={<RefreshCw size={20} />}
|
||
colorScheme="blue"
|
||
size="lg"
|
||
borderRadius="full"
|
||
boxShadow="2xl"
|
||
onClick={() => fetchDailyAnalysis(dateStr)}
|
||
isLoading={loading}
|
||
/>
|
||
</Tooltip>
|
||
<Tooltip label="回到顶部" placement="left">
|
||
<IconButton
|
||
icon={<ChevronUp size={20} />}
|
||
colorScheme="gray"
|
||
size="lg"
|
||
borderRadius="full"
|
||
boxShadow="2xl"
|
||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||
/>
|
||
</Tooltip>
|
||
</VStack>
|
||
</Box>
|
||
|
||
</Box>
|
||
);
|
||
} |