diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index 42693b04..619e1c7e 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -1,33 +1,71 @@ // src/views/Community/components/HeroPanel.js -// 顶部说明面板组件 - 驹形克己风格设计 -// 特点:极简留白、柔和几何、淡雅配色、层叠透明、诗意简洁 +// 顶部说明面板组件:事件中心 + 沪深指数K线图 + 热门概念3D动画 -import React, { useEffect, useState, useMemo, useCallback } from 'react'; +import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import { Box, + Card, + CardBody, Flex, VStack, HStack, Text, + Heading, + useColorModeValue, + SimpleGrid, + Icon, Spinner, Center, Tooltip, - Collapse, - useDisclosure, } from '@chakra-ui/react'; -import { Info, ChevronDown, ChevronUp } from 'lucide-react'; +import { AlertCircle, Clock, TrendingUp } from 'lucide-react'; import ReactECharts from 'echarts-for-react'; import { logger } from '../../../utils/logger'; +// 定义动画 +const animations = ` + @keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(1.1); } + } + @keyframes float3d { + 0%, 100% { transform: translateY(0) translateZ(0) rotateX(0deg); } + 25% { transform: translateY(-8px) translateZ(20px) rotateX(5deg); } + 50% { transform: translateY(-4px) translateZ(10px) rotateX(0deg); } + 75% { transform: translateY(-12px) translateZ(30px) rotateX(-5deg); } + } + @keyframes glow { + 0%, 100% { box-shadow: 0 0 5px currentColor, 0 0 10px currentColor; } + 50% { box-shadow: 0 0 20px currentColor, 0 0 30px currentColor, 0 0 40px currentColor; } + } + @keyframes orbit { + 0% { transform: rotate(0deg) translateX(120px) rotate(0deg); } + 100% { transform: rotate(360deg) translateX(120px) rotate(-360deg); } + } + @keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } + } +`; + +// 注入样式 +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(`/api/index/${indexCode}/kline?type=daily`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); logger.debug('HeroPanel', 'fetchIndexKline success', { indexCode, dataLength: data?.data?.length }); return data; @@ -45,12 +83,13 @@ const fetchPopularConcepts = async () => { const response = await fetch('/concept-api/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: '', size: 30, page: 1, sort_by: 'change_pct' }) + body: JSON.stringify({ query: '', size: 60, page: 1, sort_by: 'change_pct' }) }); const data = await response.json(); - if (data.results && data.results.length > 0) { + if (data.results?.length > 0) { return data.results.map(item => ({ name: item.concept, + value: Math.abs(item.price_info?.avg_change_pct || 1) + 5, change_pct: item.price_info?.avg_change_pct || 0, })); } @@ -66,21 +105,23 @@ const fetchPopularConcepts = async () => { */ const isInTradingTime = () => { const now = new Date(); - const hours = now.getHours(); - const minutes = now.getMinutes(); - const timeInMinutes = hours * 60 + minutes; + const timeInMinutes = now.getHours() * 60 + now.getMinutes(); return timeInMinutes >= 570 && timeInMinutes <= 900; }; /** - * 极简指数卡片 - 驹形克己风格 + * 迷你K线图组件(保持原有样式) */ -const MinimalIndexCard = ({ indexCode, indexName, accentColor }) => { +const MiniIndexChart = ({ indexCode, indexName }) => { const [chartData, setChartData] = useState(null); const [loading, setLoading] = useState(true); const [latestData, setLatestData] = useState(null); + const [currentDate, setCurrentDate] = useState(''); - const loadData = useCallback(async () => { + const upColor = '#ec0000'; + const downColor = '#00da3c'; + + const loadDailyData = useCallback(async () => { const data = await fetchIndexKline(indexCode); if (data?.data?.length > 0) { const latest = data.data[data.data.length - 1]; @@ -90,470 +131,558 @@ const MinimalIndexCard = ({ indexCode, indexName, accentColor }) => { change: prevClose ? (((latest.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00', isPositive: latest.close >= prevClose }); - const recentData = data.data.slice(-30); + setCurrentDate(latest.time); + const recentData = data.data.slice(-60); setChartData({ dates: recentData.map(item => item.time), - values: recentData.map(item => item.close), + klineData: recentData.map(item => [item.open, item.close, item.low, item.high]), + rawData: recentData }); } setLoading(false); }, [indexCode]); + const loadMinuteData = useCallback(async () => { + try { + const response = await fetch(`/api/index/${indexCode}/kline?type=minute`); + if (!response.ok) return; + const data = await response.json(); + if (data?.data?.length > 0) { + const latest = data.data[data.data.length - 1]; + const dayOpen = data.data[0].open; + setLatestData({ + close: latest.close, + change: dayOpen ? (((latest.close - dayOpen) / dayOpen) * 100).toFixed(2) : '0.00', + isPositive: latest.close >= dayOpen + }); + } + } catch (error) { + logger.error('HeroPanel', 'loadMinuteData error', error); + } + }, [indexCode]); + useEffect(() => { - loadData(); - }, [loadData]); + let isMounted = true; + let intervalId = null; + const init = async () => { + setLoading(true); + await loadDailyData(); + if (isMounted && isInTradingTime()) await loadMinuteData(); + if (isMounted) setLoading(false); + }; + init(); + if (isInTradingTime()) { + intervalId = setInterval(() => { + if (isInTradingTime()) loadMinuteData(); + else if (intervalId) clearInterval(intervalId); + }, 60000); + } + return () => { isMounted = false; if (intervalId) clearInterval(intervalId); }; + }, [indexCode, loadDailyData, loadMinuteData]); const chartOption = useMemo(() => { if (!chartData) return {}; return { - grid: { left: 0, right: 0, top: 0, bottom: 0 }, - xAxis: { type: 'category', show: false, data: chartData.dates }, + backgroundColor: 'transparent', + grid: { left: 10, right: 10, top: 5, bottom: 20, containLabel: false }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'cross', lineStyle: { color: 'rgba(255, 215, 0, 0.5)', width: 1, type: 'dashed' } }, + backgroundColor: 'rgba(20, 20, 20, 0.95)', + borderColor: '#FFD700', + borderWidth: 1, + textStyle: { color: '#fff', fontSize: 11, fontFamily: 'monospace' }, + padding: [8, 12], + formatter: (params) => { + const idx = params[0].dataIndex; + const raw = chartData.rawData[idx]; + if (!raw) return ''; + const prevClose = raw.prev_close || raw.open; + const change = raw.close - prevClose; + const changePct = prevClose ? ((change / prevClose) * 100).toFixed(2) : '0.00'; + const isUp = raw.close >= prevClose; + const color = isUp ? '#ec0000' : '#00da3c'; + const sign = isUp ? '+' : ''; + return `