// src/views/Community/components/HeroPanel.js // 顶部说明面板组件:事件中心 + 沪深指数K线图 + 热门概念3D动画 // 交易时间内自动更新指数行情(每分钟一次) import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { Box, Card, CardBody, Flex, VStack, HStack, Text, Heading, useColorModeValue, useDisclosure, Icon, Spinner, Center, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Tooltip, } from '@chakra-ui/react'; import { AlertCircle, Clock, TrendingUp, Info, RefreshCw } from 'lucide-react'; import ReactECharts from 'echarts-for-react'; import { logger } from '@utils/logger'; import { getApiBase } from '@utils/apiConfig'; import { useIndexQuote } from '@hooks/useIndexQuote'; import conceptStaticService from '@services/conceptStaticService'; // 定义动画 const animations = ` @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(1.1); } } @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } @keyframes floatSlow { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } `; // 注入样式 if (typeof document !== 'undefined') { const styleId = 'hero-panel-animations'; if (!document.getElementById(styleId)) { const styleSheet = document.createElement('style'); styleSheet.id = styleId; styleSheet.innerText = animations; document.head.appendChild(styleSheet); } } /** * 获取指数行情数据(日线数据) */ const fetchIndexKline = async (indexCode) => { try { const response = await fetch(`${getApiBase()}/api/index/${indexCode}/kline?type=daily`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); return data; } catch (error) { logger.error('HeroPanel', 'fetchIndexKline error', { indexCode, error: error.message }); return null; } }; /** * 获取热门概念数据(使用静态文件) */ const fetchPopularConcepts = async () => { try { const result = await conceptStaticService.fetchPopularConcepts(); if (result.success && result.data?.results?.length > 0) { return result.data.results.map(item => ({ name: item.concept, change_pct: item.price_info?.avg_change_pct || 0, })); } return []; } catch (error) { logger.error('HeroPanel', 'fetchPopularConcepts error', error); return []; } }; /** * 判断当前是否在交易时间内 */ const isInTradingTime = () => { const now = new Date(); const timeInMinutes = now.getHours() * 60 + now.getMinutes(); return timeInMinutes >= 570 && timeInMinutes <= 900; }; /** * 精美K线指数卡片 - 类似 KLineChartModal 风格 * 交易时间内自动更新实时行情(每分钟一次) */ const CompactIndexCard = ({ indexCode, indexName }) => { const [chartData, setChartData] = useState(null); const [loading, setLoading] = useState(true); const [latestData, setLatestData] = useState(null); const upColor = '#ef5350'; // 涨 - 红色 const downColor = '#26a69a'; // 跌 - 绿色 // 使用实时行情 Hook - 交易时间内每分钟自动更新 const { quote, isTrading, refresh: refreshQuote } = useIndexQuote(indexCode, { refreshInterval: 60000, // 1分钟 autoRefresh: true, }); // 加载日K线图数据 const loadChartData = useCallback(async () => { const data = await fetchIndexKline(indexCode); if (data?.data?.length > 0) { const recentData = data.data.slice(-60); // 最近60天 setChartData({ dates: recentData.map(item => item.time), klineData: recentData.map(item => [item.open, item.close, item.low, item.high]), volumes: recentData.map(item => item.volume || 0), rawData: recentData }); // 如果没有实时行情,使用日线数据的最新值 if (!quote) { const latest = data.data[data.data.length - 1]; const prevClose = latest.prev_close || data.data[data.data.length - 2]?.close || latest.open; const changeAmount = latest.close - prevClose; const changePct = prevClose ? ((changeAmount / prevClose) * 100) : 0; setLatestData({ close: latest.close, open: latest.open, high: latest.high, low: latest.low, changeAmount: changeAmount, changePct: changePct, isPositive: changeAmount >= 0 }); } } setLoading(false); }, [indexCode, quote]); // 初始加载日K数据 useEffect(() => { loadChartData(); }, [loadChartData]); // 当实时行情更新时,更新 latestData useEffect(() => { if (quote) { setLatestData({ close: quote.price, open: quote.open, high: quote.high, low: quote.low, changeAmount: quote.change, changePct: quote.change_pct, isPositive: quote.change >= 0, updateTime: quote.update_time, isRealtime: true, }); } }, [quote]); const chartOption = useMemo(() => { if (!chartData) return {}; return { backgroundColor: 'transparent', grid: [ { left: 0, right: 0, top: 8, bottom: 28, containLabel: false }, { left: 0, right: 0, top: '75%', bottom: 4, containLabel: false } ], tooltip: { trigger: 'axis', axisPointer: { type: 'cross', crossStyle: { color: 'rgba(255, 215, 0, 0.6)', width: 1 }, lineStyle: { color: 'rgba(255, 215, 0, 0.4)', width: 1, type: 'dashed' } }, backgroundColor: 'rgba(15, 15, 25, 0.98)', borderColor: 'rgba(255, 215, 0, 0.5)', borderWidth: 1, borderRadius: 8, padding: [12, 16], textStyle: { color: '#e0e0e0', fontSize: 12 }, extraCssText: 'box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);', formatter: (params) => { const idx = params[0]?.dataIndex; if (idx === undefined) return ''; const raw = chartData.rawData[idx]; if (!raw) return ''; // 安全格式化数字 const safeFixed = (val, digits = 2) => (val != null && !isNaN(val)) ? val.toFixed(digits) : '-'; // 计算涨跌 const prevClose = raw.prev_close || (idx > 0 ? chartData.rawData[idx - 1]?.close : raw.open) || raw.open; const changeAmount = (raw.close != null && prevClose != null) ? (raw.close - prevClose) : 0; const changePct = prevClose ? ((changeAmount / prevClose) * 100) : 0; const isUp = changeAmount >= 0; const color = isUp ? '#ef5350' : '#26a69a'; const sign = isUp ? '+' : ''; return `