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 `
📅 ${raw.time}
+
+ 开盘${raw.open.toFixed(2)} + 最高${raw.high.toFixed(2)} + 最低${raw.low.toFixed(2)} + 收盘${raw.close.toFixed(2)} + 涨跌幅${sign}${changePct}% +
`; + } + }, + xAxis: { type: 'category', data: chartData.dates, show: false }, yAxis: { type: 'value', show: false, scale: true }, series: [{ - type: 'line', - data: chartData.values, - smooth: true, - symbol: 'none', - lineStyle: { - width: 2, - color: latestData?.isPositive ? 'rgba(239, 68, 68, 0.6)' : 'rgba(34, 197, 94, 0.6)' - }, - areaStyle: { - color: { - type: 'linear', - x: 0, y: 0, x2: 0, y2: 1, - colorStops: [ - { offset: 0, color: latestData?.isPositive ? 'rgba(239, 68, 68, 0.15)' : 'rgba(34, 197, 94, 0.15)' }, - { offset: 1, color: 'transparent' } - ] - } - } + type: 'candlestick', + data: chartData.klineData, + itemStyle: { color: upColor, color0: downColor, borderColor: upColor, borderColor0: downColor }, + barWidth: '60%' }] }; - }, [chartData, latestData]); + }, [chartData, upColor, downColor]); - if (loading) { - return ( -
- -
- ); - } + if (loading) return
; return ( - - {/* 背景图表 */} - - - - - {/* 数据展示 */} - + + - - {indexName} - - + {indexName} + {latestData?.close.toFixed(2)} + 📅 {currentDate} - - - {latestData?.isPositive ? '+' : ''}{latestData?.change}% - - - + + + {latestData?.isPositive ? '↗' : '↘'} {latestData?.isPositive ? '+' : ''}{latestData?.change}% + + {isInTradingTime() && ( + + + 实时更新 + + )} + + + + + + ); }; /** - * 概念流动标签 - 极简风格 + * 3D 球形概念云动画组件 */ -const ConceptFlow = () => { +const Concept3DSphere = () => { 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); useEffect(() => { - const loadConcepts = async () => { + const load = async () => { const data = await fetchPopularConcepts(); - setConcepts(data); + setConcepts(data.slice(0, 30)); // 取前30个 setLoading(false); }; - loadConcepts(); + 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'; + if (pct > 0) return '#ff8a80'; + if (pct === 0) return '#FFD700'; + if (pct > -2) return '#69f0ae'; + if (pct > -5) return '#00e676'; + return '#00c853'; + }; + const handleClick = (name) => { window.open(`https://valuefrontier.cn/htmls/${name}.html`, '_blank'); }; - if (loading) { - return ( -
- -
- ); - } + if (loading) return
; - // 取前12个概念 - const displayConcepts = concepts.slice(0, 12); + // 计算球面坐标 + 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) + }; + }); - return ( - - {displayConcepts.map((concept, idx) => { - const isPositive = concept.change_pct >= 0; - return ( - handleClick(concept.name)} - > - - - {concept.name} - - - {isPositive ? '+' : ''}{concept.change_pct.toFixed(1)}% - - - - ); - })} - - ); -}; - -/** - * 顶部说明面板主组件 - 驹形克己风格 - */ -const HeroPanel = () => { - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); + // 应用旋转 + 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 }; + }); return ( setIsHovered(true)} + onMouseLeave={() => { setIsHovered(false); setHoveredIdx(null); }} + style={{ perspective: '800px' }} > - {/* 主容器 - 极简玻璃态 */} + {/* 中心发光圆 */} - {/* 装饰性几何元素 - 驹形克己风格 */} - {/* 右上角大圆 */} - - {/* 左下角半圆 */} - - {/* 中间小圆点 */} - - + position="absolute" + top="50%" + left="50%" + transform="translate(-50%, -50%)" + w="60px" + h="60px" + borderRadius="full" + bg="radial-gradient(circle, rgba(255,215,0,0.3) 0%, transparent 70%)" + animation="pulse 3s ease-in-out infinite" + /> - {/* 内容区域 */} - - {/* 顶部标题行 */} - { + 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 color = getColor(concept.change_pct); + const isActive = hoveredIdx === idx; + + return ( + setHoveredIdx(idx)} + onMouseLeave={() => setHoveredIdx(null)} + onClick={() => handleClick(concept.name)} > - {/* 左侧:标题 */} - - + {concept.name} + + {isActive && ( + - 事件中心 - - {isInTradingTime() && ( - - - - 交易中 - - - )} - + + {concept.change_pct > 0 ? '+' : ''}{concept.change_pct.toFixed(2)}% + + + )} + + ); + })} - {/* 右侧:说明按钮 */} - - - - 使用说明 - - {isOpen ? ( - - ) : ( - - )} - - - - {/* 可折叠的说明区域 */} - - - - {/* 评级说明 */} - - - - - 关于事件评级 - - - 重要度 - - S · A · B · C - - 由大模型基于事件影响力评估,与收益率预测无关。事件包含 - 利好 - 和 - 利空 - 两类,请在「历史相关事件」中查看历史表现来判断投资价值。 - - - - - {/* 延迟提醒 */} - - - - - 延迟提醒 - - - 模型需回溯历史数据,事件结果会有 - - 2-3 分钟 - - 延迟,请 - - 不要追高 - - - - - - - - - {/* 三栏布局 */} - - {/* 左侧:上证指数 */} - - - - - {/* 中间:深证成指 */} - - - - - {/* 右侧:热门概念 */} - - - - - 热门概念 - - - 点击查看详情 - - - - - - - - - - + {/* 轨道环 */} + + ); }; +/** + * 使用说明卡片组件 + */ +const InfoCard = () => { + return ( + + + {/* 标题 */} + + + + + + 使用说明 + + + + {/* 评级说明 */} + + + + 重要度 SABC + {" "}由大模型基于事件影响力评估,与收益率预测无关 + + + + {/* 筛选说明 */} + + + + 事件包含 + 利好 + 和 + 利空 + ,请在「历史相关事件」中查看历史表现 + + + + {/* 延迟提醒 */} + + + + 数据延迟 + 2-3分钟 + ,请 + 不要追高 + + + + + ); +}; + +/** + * 顶部说明面板主组件 + */ +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)'); + + return ( + + {/* 装饰性光晕 */} + + + + + {/* 顶部标题行 */} + + + + + 事件中心 + + + {isInTradingTime() && ( + + + + 交易时段 + + + )} + + + + + {/* 左侧:使用说明 */} + + + + + {/* 中间:沪深指数K线图 */} + + + + + + + + + + + + {/* 右侧:3D概念球 */} + + + + + + + 热门概念 + + + + 点击查看详情 + + + + + + + + + + + ); +}; + export default HeroPanel;