Files
vf_react/src/views/LimitAnalyse/index.js
zdl 2207a680b5 refactor(icons): 迁移其他 views 目录图标到 lucide-react
- views/Center, views/Community, views/DataBrowser 等
- views/EventDetail, views/LimitAnalyse, views/StockOverview
- views/TradingSimulation, views/Pages, views/Authentication
- views/Profile, views/Settings
- 处理 Tag/TagIcon 命名冲突
- 涉及 52 个组件文件

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 13:00:41 +08:00

438 lines
18 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,
HStack,
Heading,
Text,
Badge,
useToast,
Skeleton,
IconButton,
Flex,
useColorModeValue,
SimpleGrid,
Tooltip,
Card,
CardBody,
Alert,
AlertIcon,
} from '@chakra-ui/react';
import { RefreshCw, ChevronUp } from 'lucide-react';
// 导入拆分的组件
// 注意:在实际使用中,这些组件应该被拆分到独立的文件中
// 这里为了演示,我们假设它们已经被正确导出
// 使用静态数据服务(从 /data/zt/ 读取 JSON 文件)
import ztStaticService from '../../services/ztStaticService';
// 导入的组件(实际使用时应该从独立文件导入)
// 恢复使用本页自带的轻量日历
import EnhancedCalendar from './components/EnhancedCalendar';
import SectorDetails from './components/SectorDetails';
import { DataAnalysis } 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(null); // null 表示未加载,[] 表示加载完成但无数据
const [wordCloudData, setWordCloudData] = useState([]);
const [searchResults, setSearchResults] = useState(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const toast = useToast();
// 🎯 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);
// 🎯 追踪日期选择
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;
};
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 (
<Box minH="100vh" bg={bgColor}>
{/* 导航栏已由 MainLayout 提供 */}
{/* 顶部Header */}
<Box bgGradient="linear(to-br, blue.500, purple.600)" color="white" py={8} px={6} borderRadius="xl">
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6} alignItems="stretch">
{/* 左侧:标题置顶,注释与图例贴底 */}
<Flex direction="column" minH="420px" justify="space-between">
<VStack align="start" spacing={4}>
<HStack>
<Badge colorScheme="whiteAlpha" fontSize="sm" px={2}>
AI驱动
</Badge>
<Badge colorScheme="yellow" fontSize="sm" px={2}>
实时更新
</Badge>
</HStack>
<Heading
size="3xl"
fontWeight="extrabold"
letterSpacing="-0.5px"
lineHeight="shorter"
textShadow="0 6px 24px rgba(0,0,0,0.25)"
>
涨停板块分析平台
</Heading>
<Text fontSize="xl" opacity={0.98} fontWeight="semibold" textShadow="0 4px 16px rgba(0,0,0,0.2)">
以大模型辅助整理海量信息结合领域知识图谱与分析师复核呈现涨停板块关键线索
</Text>
</VStack>
<VStack align="stretch" spacing={3}>
<Alert
status="info"
borderRadius="xl"
bg="whiteAlpha.200"
color="whiteAlpha.900"
borderWidth="1px"
borderColor="whiteAlpha.300"
backdropFilter="saturate(180%) blur(10px)"
boxShadow="0 8px 32px rgba(0,0,0,0.2)"
>
<AlertIcon />
<Text fontSize="md" fontWeight="medium">
{selectedDate ? `当前选择:${formatDisplayDate(selectedDate)}` : '当前选择:--'}
{getSelectedDateCount() != null ? ` - ${getSelectedDateCount()}只涨停` : ''}
</Text>
</Alert>
<Card
bg="whiteAlpha.200"
color="whiteAlpha.900"
borderRadius="xl"
boxShadow="0 8px 32px rgba(0,0,0,0.2)"
borderWidth="1px"
borderColor="whiteAlpha.300"
backdropFilter="saturate(180%) blur(10px)"
w="full"
>
<CardBody>
<VStack align="stretch" spacing={2}>
<Heading size="sm" color="whiteAlpha.900">涨停数量图例</Heading>
<VStack align="stretch" spacing={2}>
<HStack>
<Box w={5} h={5} bg="green.200" borderRadius="md" border="1px solid" borderColor="whiteAlpha.400" />
<Text fontSize="sm">少量 (50)</Text>
</HStack>
<HStack>
<Box w={5} h={5} bg="yellow.200" borderRadius="md" border="1px solid" borderColor="whiteAlpha.400" />
<Text fontSize="sm">中等 (51-80)</Text>
</HStack>
<HStack>
<Box w={5} h={5} bg="red.200" borderRadius="md" border="1px solid" borderColor="whiteAlpha.400" />
<Text fontSize="sm">大量 (&gt;80)</Text>
</HStack>
</VStack>
</VStack>
</CardBody>
</Card>
</VStack>
</Flex>
{/* 右侧:半屏日历 */}
<Card
bg="whiteAlpha.200"
borderRadius="xl"
boxShadow="0 8px 32px rgba(0,0,0,0.2)"
borderWidth="1px"
borderColor="whiteAlpha.300"
backdropFilter="saturate(180%) blur(10px)"
w="full"
>
<CardBody p={4}>
<EnhancedCalendar
selectedDate={selectedDate}
onDateChange={handleDateChange}
availableDates={availableDates}
compact
hideSelectionInfo
hideLegend
width="100%"
cellHeight={16}
/>
</CardBody>
</Card>
</SimpleGrid>
</Box>
{/* 主内容区 - padding 由 MainLayout 统一设置 */}
<Box py={8}>
{/* 搜索框 */}
<AdvancedSearch onSearch={handleSearch} loading={loading} />
{/* 数据分析(含涨停统计) */}
{loading ? (
<Skeleton height="500px" borderRadius="xl" mb={6} />
) : (
<Box mb={6}>
<DataAnalysis
dailyData={dailyData}
wordCloudData={wordCloudData}
totalStocks={dailyData?.total_stocks || 0}
dateStr={dateStr}
/>
</Box>
)}
{/* 板块详情 - 核心内容 */}
{loading ? (
<Skeleton height="600px" borderRadius="xl" mb={6} />
) : (
<Box mb={6}>
<SectorDetails
sortedSectors={getSortedSectorData()}
totalStocks={dailyData?.total_stocks || 0}
/>
</Box>
)}
{/* 高位股统计 */}
<HighPositionStocks 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>
);
}