diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index 619e1c7e..4f87893d 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -12,13 +12,11 @@ import { Text, Heading, useColorModeValue, - SimpleGrid, Icon, Spinner, Center, - Tooltip, } from '@chakra-ui/react'; -import { AlertCircle, Clock, TrendingUp } from 'lucide-react'; +import { AlertCircle, Clock, TrendingUp, Info } from 'lucide-react'; import ReactECharts from 'echarts-for-react'; import { logger } from '../../../utils/logger'; @@ -28,24 +26,14 @@ const animations = ` 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; } } + @keyframes floatSlow { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } + } `; // 注入样式 @@ -67,7 +55,6 @@ const fetchIndexKline = async (indexCode) => { const response = await fetch(`/api/index/${indexCode}/kline?type=daily`); 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; } catch (error) { logger.error('HeroPanel', 'fetchIndexKline error', { indexCode, error: error.message }); @@ -89,7 +76,6 @@ const fetchPopularConcepts = async () => { 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, })); } @@ -110,18 +96,17 @@ const isInTradingTime = () => { }; /** - * 迷你K线图组件(保持原有样式) + * 紧凑型K线指数卡片 */ -const MiniIndexChart = ({ indexCode, indexName }) => { +const CompactIndexCard = ({ indexCode, indexName }) => { const [chartData, setChartData] = useState(null); const [loading, setLoading] = useState(true); const [latestData, setLatestData] = useState(null); - const [currentDate, setCurrentDate] = useState(''); const upColor = '#ec0000'; const downColor = '#00da3c'; - const loadDailyData = useCallback(async () => { + const loadData = useCallback(async () => { const data = await fetchIndexKline(indexCode); if (data?.data?.length > 0) { const latest = data.data[data.data.length - 1]; @@ -131,8 +116,7 @@ const MiniIndexChart = ({ indexCode, indexName }) => { change: prevClose ? (((latest.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00', isPositive: latest.close >= prevClose }); - setCurrentDate(latest.time); - const recentData = data.data.slice(-60); + const recentData = data.data.slice(-40); setChartData({ dates: recentData.map(item => item.time), klineData: recentData.map(item => [item.open, item.close, item.low, item.high]), @@ -142,75 +126,32 @@ const MiniIndexChart = ({ indexCode, indexName }) => { 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(() => { - 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]); + loadData(); + }, [loadData]); const chartOption = useMemo(() => { if (!chartData) return {}; return { backgroundColor: 'transparent', - grid: { left: 10, right: 10, top: 5, bottom: 20, containLabel: false }, + grid: { left: 5, right: 5, top: 5, bottom: 5, 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], + textStyle: { color: '#fff', fontSize: 10, fontFamily: 'monospace' }, + padding: [6, 10], 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 changePct = prevClose ? (((raw.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00'; const isUp = raw.close >= prevClose; const color = isUp ? '#ec0000' : '#00da3c'; - const sign = isUp ? '+' : ''; - return `
📅 ${raw.time}
-
- 开盘${raw.open.toFixed(2)} - 最高${raw.high.toFixed(2)} - 最低${raw.low.toFixed(2)} - 收盘${raw.close.toFixed(2)} - 涨跌幅${sign}${changePct}% -
`; + return `
${raw.time}
收盘: ${raw.close.toFixed(2)}
${isUp ? '+' : ''}${changePct}%
`; } }, xAxis: { type: 'category', data: chartData.dates, show: false }, @@ -219,76 +160,56 @@ const MiniIndexChart = ({ indexCode, indexName }) => { type: 'candlestick', data: chartData.klineData, itemStyle: { color: upColor, color0: downColor, borderColor: upColor, borderColor0: downColor }, - barWidth: '60%' + barWidth: '70%' }] }; }, [chartData, upColor, downColor]); - if (loading) return
; + if (loading) return
; return ( - - - - {indexName} - - {latestData?.close.toFixed(2)} - - 📅 {currentDate} - - - - {latestData?.isPositive ? '↗' : '↘'} {latestData?.isPositive ? '+' : ''}{latestData?.change}% - - {isInTradingTime() && ( - - - 实时更新 - - )} - - - - + + {/* 左侧数据 */} + + {indexName} + + {latestData?.close.toFixed(2)} + + + {latestData?.isPositive ? '▲' : '▼'} {latestData?.isPositive ? '+' : ''}{latestData?.change}% + + + {/* 右侧K线图 */} + + - + ); }; /** - * 3D 球形概念云动画组件 + * 概念漂浮动画 - 稀疏美观版 */ -const Concept3DSphere = () => { +const ConceptFloat = () => { const [concepts, setConcepts] = useState([]); const [loading, setLoading] = useState(true); - const containerRef = useRef(null); - const [rotation, setRotation] = useState({ x: 0, y: 0 }); - const [isHovered, setIsHovered] = useState(false); const [hoveredIdx, setHoveredIdx] = useState(null); + const containerRef = useRef(null); useEffect(() => { const load = async () => { const data = await fetchPopularConcepts(); - setConcepts(data.slice(0, 30)); // 取前30个 + // 只取前12个,更稀疏 + setConcepts(data.slice(0, 12)); setLoading(false); }; load(); }, []); - // 自动旋转 - useEffect(() => { - if (isHovered) return; - const interval = setInterval(() => { - setRotation(prev => ({ x: prev.x, y: prev.y + 0.5 })); - }, 50); - return () => clearInterval(interval); - }, [isHovered]); - const getColor = (pct) => { if (pct > 5) return '#ff1744'; if (pct > 2) return '#ff5252'; @@ -303,90 +224,65 @@ const Concept3DSphere = () => { window.open(`https://valuefrontier.cn/htmls/${name}.html`, '_blank'); }; - if (loading) return
; + if (loading) return
; - // 计算球面坐标 - const radius = 90; - const positions = concepts.map((_, i) => { - const phi = Math.acos(-1 + (2 * i) / concepts.length); - const theta = Math.sqrt(concepts.length * Math.PI) * phi; - return { - x: radius * Math.cos(theta) * Math.sin(phi), - y: radius * Math.sin(theta) * Math.sin(phi), - z: radius * Math.cos(phi) - }; - }); - - // 应用旋转 - const rotatedPositions = positions.map(pos => { - const cosY = Math.cos(rotation.y * Math.PI / 180); - const sinY = Math.sin(rotation.y * Math.PI / 180); - const x1 = pos.x * cosY - pos.z * sinY; - const z1 = pos.x * sinY + pos.z * cosY; - return { x: x1, y: pos.y, z: z1 }; - }); + // 预设12个位置,均匀分布且不重叠 + const positions = [ + { x: 15, y: 20 }, { x: 50, y: 15 }, { x: 85, y: 25 }, + { x: 25, y: 50 }, { x: 60, y: 45 }, { x: 80, y: 55 }, + { x: 10, y: 75 }, { x: 40, y: 80 }, { x: 70, y: 75 }, + { x: 20, y: 35 }, { x: 55, y: 65 }, { x: 90, y: 40 }, + ]; return ( setIsHovered(true)} - onMouseLeave={() => { setIsHovered(false); setHoveredIdx(null); }} - style={{ perspective: '800px' }} + overflow="hidden" > - {/* 中心发光圆 */} - - {/* 概念标签 */} {concepts.map((concept, idx) => { - const pos = rotatedPositions[idx]; - const scale = (pos.z + radius) / (2 * radius); // 0-1, 越近越大 - const opacity = 0.3 + scale * 0.7; - const size = 10 + scale * 6; - const zIndex = Math.round(scale * 100); + const pos = positions[idx] || { x: 50, y: 50 }; const color = getColor(concept.change_pct); const isActive = hoveredIdx === idx; + const size = 11 + (idx % 3) * 2; // 11-15px return ( setHoveredIdx(idx)} onMouseLeave={() => setHoveredIdx(null)} onClick={() => handleClick(concept.name)} + animation={`floatSlow ${5 + (idx % 3)}s ease-in-out infinite ${idx * 0.3}s`} > {concept.name} + + {/* 悬停提示 */} {isActive && ( { ); })} - - {/* 轨道环 */} - - ); }; /** - * 使用说明卡片组件 + * 极简提示标签 - 悬停显示详情 */ -const InfoCard = () => { +const InfoTooltip = () => { + const [isOpen, setIsOpen] = useState(false); + return ( - - - {/* 标题 */} - - - - - - 使用说明 - - + + {/* 触发器:小标签 */} + setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + onClick={() => setIsOpen(!isOpen)} + > + + + 使用说明 + + - {/* 评级说明 */} - - - - 重要度 SABC - {" "}由大模型基于事件影响力评估,与收益率预测无关 - - + {/* 悬浮提示框 */} + setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + > + {/* 小箭头 */} + - {/* 筛选说明 */} - - - - 事件包含 - 利好 - 和 - 利空 - ,请在「历史相关事件」中查看历史表现 - - + + {/* SABC说明 */} + + SABC + + 基于事件影响力评级,非收益预测 + + - {/* 延迟提醒 */} - - - - 数据延迟 - 2-3分钟 - ,请 - 不要追高 - - - + {/* 涨跌幅说明 */} + + 涨跌幅 + + 新闻发布时股价 → 当前价格的变化 + + + + {/* 延迟提醒 */} + + + + + + 数据延迟 2-3分钟 + ,切勿追高 + + + + {/* 分隔线 */} + + + {/* 盘前新闻 */} + + + + 关注盘前新闻 + (收盘后至次日开盘前的消息) + + + + {/* 利好利空 */} + + + + 事件含 + 利好/ + 利空 + ,需自行判断 + + + + ); }; @@ -524,14 +430,14 @@ const InfoCard = () => { */ const HeroPanel = () => { const gradientBg = 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 25%, #16213e 50%, #1a1a2e 75%, #0a0a0a 100%)'; - const borderColor = useColorModeValue('rgba(255, 215, 0, 0.4)', 'rgba(255, 215, 0, 0.3)'); + const borderColor = useColorModeValue('rgba(255, 215, 0, 0.3)', 'rgba(255, 215, 0, 0.25)'); return ( { {/* 装饰性光晕 */} - - {/* 顶部标题行 */} - - + + {/* 标题行:标题 + 使用说明 + 交易状态 */} + + { 事件中心 - {isInTradingTime() && ( - - - - 交易时段 - - - )} + {/* 使用说明 - 悬浮提示 */} + + + {/* 右侧:交易状态 */} + {isInTradingTime() && ( + + + + 交易中 + + + )} - - {/* 左侧:使用说明 */} - - - + {/* 内容区:指数 + 概念 */} + + {/* 左侧:双指数横向排列 */} + + {/* 上证指数 */} + + + - {/* 中间:沪深指数K线图 */} - - - - - - - - - - + {/* 深证成指 */} + + + + - {/* 右侧:3D概念球 */} - - - - - - - 热门概念 - - - - 点击查看详情 - - - - - - + {/* 右侧:热门概念 */} + + {/* 标题 */} + + + + 热门概念 + + + 点击查看 + + + + + + - + );