Files
vf_react/src/views/LimitAnalyse/index.js
zdl e5f0d9aa2b refactor(LimitAnalyse): 重构主页面布局与数据服务
- 整合市场全景、板块异动、高位股统计模块
  - 状态提升实现板块点击联动(selectedSector)
  - 更新 ztStaticService 静态数据服务:
    - 添加缓存机制(dates 5分钟、daily 30分钟)
    - 转换 stock_codes 为完整 stocks 对象
    - 支持 sector_relations 板块关联数据
  - 更新 Mock handlers:
    - 完善 dates.json / daily/{date}.json 静态路径
    - 添加 sector_relations 网络图数据生成
    - 支持 chart_data 饼图数据结构
2026-01-05 14:34:19 +08:00

311 lines
11 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

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, { 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>
);
}