diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index 3312d18f..42693b04 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -1,55 +1,29 @@ // src/views/Community/components/HeroPanel.js -// 顶部说明面板组件:产品功能介绍 + 沪深指数折线图 + 热门概念词云图 +// 顶部说明面板组件 - 驹形克己风格设计 +// 特点:极简留白、柔和几何、淡雅配色、层叠透明、诗意简洁 import React, { useEffect, useState, useMemo, useCallback } from 'react'; - -// 定义 pulse 动画 -const pulseAnimation = ` - @keyframes pulse { - 0%, 100% { - opacity: 1; - transform: scale(1); - } - 50% { - opacity: 0.6; - transform: scale(1.1); - } - } -`; - -// 注入样式到页面 -if (typeof document !== 'undefined') { - const styleSheet = document.createElement('style'); - styleSheet.type = 'text/css'; - styleSheet.innerText = pulseAnimation; - document.head.appendChild(styleSheet); -} import { Box, - Card, - CardBody, Flex, VStack, HStack, Text, - Heading, - useColorModeValue, - SimpleGrid, - Icon, Spinner, Center, + Tooltip, + Collapse, + useDisclosure, } from '@chakra-ui/react'; -import { TrendingUp, Activity, Globe, Zap } from 'lucide-react'; +import { Info, ChevronDown, ChevronUp } from 'lucide-react'; import ReactECharts from 'echarts-for-react'; import { logger } from '../../../utils/logger'; -import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme'; /** * 获取指数行情数据(日线数据) */ const fetchIndexKline = async (indexCode) => { try { - // 使用日线数据,获取最近60个交易日 const response = await fetch(`/api/index/${indexCode}/kline?type=daily`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -64,33 +38,19 @@ const fetchIndexKline = async (indexCode) => { }; /** - * 获取热门概念数据(用于流动动画) + * 获取热门概念数据 */ const fetchPopularConcepts = async () => { try { const response = await fetch('/concept-api/search', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: '', - size: 60, // 获取前60个概念 - page: 1, - sort_by: 'change_pct' - }) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: '', size: 30, page: 1, sort_by: 'change_pct' }) }); - const data = await response.json(); - logger.debug('HeroPanel', 'fetchPopularConcepts response', { - total: data.total, - resultsCount: data.results?.length - }); - if (data.results && 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, })); } @@ -109,1069 +69,490 @@ const isInTradingTime = () => { const hours = now.getHours(); const minutes = now.getMinutes(); const timeInMinutes = hours * 60 + minutes; - - // 9:30 - 15:00 (570分钟 - 900分钟) return timeInMinutes >= 570 && timeInMinutes <= 900; }; /** - * 迷你K线图组件(支持实时更新) + * 极简指数卡片 - 驹形克己风格 */ -const MiniIndexChart = ({ indexCode, indexName }) => { +const MinimalIndexCard = ({ indexCode, indexName, accentColor }) => { const [chartData, setChartData] = useState(null); const [loading, setLoading] = useState(true); const [latestData, setLatestData] = useState(null); - const [currentDate, setCurrentDate] = useState(''); - const chartBg = useColorModeValue('transparent', 'transparent'); - // 中国市场惯例:涨红跌绿 - const upColor = '#ec0000'; // 上涨:红色 - const downColor = '#00da3c'; // 下跌:绿色 - - // 加载日线数据 - const loadDailyData = useCallback(async () => { + const loadData = useCallback(async () => { const data = await fetchIndexKline(indexCode); - - if (data && data.data && data.data.length > 0) { - // 取最近一个交易日的数据 + if (data?.data?.length > 0) { const latest = data.data[data.data.length - 1]; const prevClose = latest.prev_close || latest.close; - setLatestData({ close: latest.close, change: prevClose ? (((latest.close - prevClose) / prevClose) * 100).toFixed(2) : '0.00', isPositive: latest.close >= prevClose }); - - setCurrentDate(latest.time); - - // 准备K线图数据(最近60个交易日) - const recentData = data.data.slice(-60); + const recentData = data.data.slice(-30); setChartData({ dates: recentData.map(item => item.time), - klineData: recentData.map(item => [ - item.open, - item.close, - item.low, - item.high - ]), - rawData: recentData // 保存原始数据用于 tooltip + values: recentData.map(item => item.close), }); } - 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.data && data.data.length > 0) { - // 取最新分钟数据 - const latest = data.data[data.data.length - 1]; - // 分钟线没有 prev_close,使用第一条数据的 open 作为开盘价 - 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 - }); - - logger.debug('HeroPanel', 'Minute data updated', { - indexCode, - close: latest.close, - time: latest.time, - change: (((latest.close - dayOpen) / dayOpen) * 100).toFixed(2) - }); - } - } 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) { - // 如果在交易时间,立即加载一次分钟数据 - if (isInTradingTime()) { - await loadMinuteData(); - } - setLoading(false); - } - }; - - init(); - - // 设置定时器:交易时间内每分钟更新 - if (isInTradingTime()) { - intervalId = setInterval(() => { - if (isInTradingTime()) { - loadMinuteData(); - } else { - // 如果超出交易时间,清除定时器 - if (intervalId) { - clearInterval(intervalId); - } - } - }, 60000); // 每60秒更新一次 - } - - return () => { - isMounted = false; - if (intervalId) { - clearInterval(intervalId); - } - }; - }, [indexCode, loadDailyData, loadMinuteData]); + loadData(); + }, [loadData]); const chartOption = useMemo(() => { if (!chartData) return {}; - return { - backgroundColor: chartBg, - 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: function (params) { - const dataIndex = params[0].dataIndex; - const rawDataItem = chartData.rawData[dataIndex]; - - if (!rawDataItem) return ''; - - const open = rawDataItem.open; - const high = rawDataItem.high; - const low = rawDataItem.low; - const close = rawDataItem.close; - const prevClose = rawDataItem.prev_close || open; - const change = close - prevClose; - const changePct = prevClose ? ((change / prevClose) * 100).toFixed(2) : '0.00'; - const isUp = close >= prevClose; - - // Bloomberg 风格格式化(涨红跌绿) - const changeColor = isUp ? '#ec0000' : '#00da3c'; - const changeSign = isUp ? '+' : ''; - - return ` -
- 📅 ${rawDataItem.time} -
-
- 开盘${open.toFixed(2)} - 最高${high.toFixed(2)} - 最低${low.toFixed(2)} - 收盘${close.toFixed(2)} - 涨跌${changeSign}${change.toFixed(2)} - 涨跌幅${changeSign}${changePct}% -
- `; - } - }, - xAxis: { - type: 'category', - data: chartData.dates, - show: false - }, - yAxis: { - type: 'value', - show: false, - scale: true - }, + grid: { left: 0, right: 0, top: 0, bottom: 0 }, + xAxis: { type: 'category', show: false, data: chartData.dates }, + yAxis: { type: 'value', show: false, scale: true }, series: [{ - type: 'candlestick', - data: chartData.klineData, - itemStyle: { - color: upColor, - color0: downColor, - borderColor: upColor, - borderColor0: downColor + 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)' }, - barWidth: '60%' + 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' } + ] + } + } }] }; - }, [chartData, chartBg, upColor, downColor]); + }, [chartData, latestData]); 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?.change}% + + + ); }; /** - * 概念流动动画组件(替代词云) + * 概念流动标签 - 极简风格 */ -const ConceptFlowAnimation = () => { +const ConceptFlow = () => { const [concepts, setConcepts] = useState([]); const [loading, setLoading] = useState(true); - const [isPaused, setIsPaused] = useState(false); - const [hoveredConcept, setHoveredConcept] = useState(null); useEffect(() => { - let isMounted = true; - const loadConcepts = async () => { - setLoading(true); const data = await fetchPopularConcepts(); - if (isMounted && data && data.length > 0) { - setConcepts(data); - } - if (isMounted) { - setLoading(false); - } + setConcepts(data); + setLoading(false); }; - loadConcepts(); - - return () => { - isMounted = false; - }; }, []); - // 根据涨跌幅获取颜色 - const getColor = (changePct) => { - if (changePct > 7) return '#ff0000'; // 超大涨:纯红 - if (changePct > 5) return '#ff1744'; // 大涨:亮红色 - if (changePct > 3) return '#ff4d4f'; // 中涨:红色 - if (changePct > 1) return '#ff7875'; // 小涨:浅红 - if (changePct > 0) return '#ffa940'; // 微涨:橙色 - if (changePct === 0) return '#FFD700'; // 平盘:金色 - if (changePct > -1) return '#73d13d'; // 微跌:浅绿 - if (changePct > -3) return '#52c41a'; // 小跌:绿色 - if (changePct > -5) return '#00c853'; // 中跌:深绿 - if (changePct > -7) return '#00a152'; // 大跌:更深绿 - return '#00796b'; // 超大跌:墨绿 - }; - - // 处理概念点击 - const handleConceptClick = (conceptName) => { - const url = `https://valuefrontier.cn/htmls/${conceptName}.html`; - window.open(url, '_blank'); + const handleClick = (name) => { + window.open(`https://valuefrontier.cn/htmls/${name}.html`, '_blank'); }; if (loading) { return ( -
- +
+
); } - if (concepts.length === 0) { - return ( -
- - 暂无热门概念数据 - -
- ); - } - - // 将 60 个概念均匀分成 3 行(每行 20 个) - const rowCount = 3; - const conceptsPerRow = Math.ceil(concepts.length / rowCount); - const rows = []; - - for (let i = 0; i < rowCount; i++) { - const start = i * conceptsPerRow; - const end = Math.min(start + conceptsPerRow, concepts.length); - rows.push(concepts.slice(start, end)); - } - - // 根据涨跌幅计算字体大小(涨跌幅越大,字体越大) - const getFontSize = (changePct) => { - const absChange = Math.abs(changePct); - if (absChange >= 10) return 'xl'; // 超大涨跌 - 20px - if (absChange >= 7) return 'lg'; // 大涨跌 - 18px - if (absChange >= 5) return 'md'; // 中涨跌 - 16px - if (absChange >= 3) return 'md'; // 小涨跌 - 16px - return 'sm'; // 微涨跌 - 14px - }; + // 取前12个概念 + const displayConcepts = concepts.slice(0, 12); return ( - - {rows.map((row, rowIndex) => { - // 每行不同的速度:第一行50秒,第二行60秒,第三行70秒(更慢) - const duration = 50 + rowIndex * 10; - + + {displayConcepts.map((concept, idx) => { + const isPositive = concept.change_pct >= 0; return ( handleClick(concept.name)} > - - {/* 渲染两次以实现无缝循环 */} - {[...row, ...row].map((concept, idx) => { - const changePct = concept.change_pct; - const color = getColor(changePct); - const sign = changePct > 0 ? '+' : ''; - const icon = changePct > 0 ? '📈' : '📉'; - const isHovered = hoveredConcept === `${rowIndex}-${idx}`; - - const fontSize = getFontSize(changePct); - const isLargeFontSize = fontSize === 'xl' || fontSize === 'lg'; - - return ( - { - setIsPaused(true); - setHoveredConcept(`${rowIndex}-${idx}`); - }} - onMouseLeave={() => { - setIsPaused(false); - setHoveredConcept(null); - }} - onClick={() => handleConceptClick(concept.name)} - _hover={{ - transform: 'scale(1.2)', - zIndex: 100 - }} + + - {/* 概念名称 */} - - {concept.name} - - - {/* 悬停时显示涨跌幅 */} - {isHovered && ( - - - - {icon} {concept.name} - - - {sign}{changePct.toFixed(2)}% - - - - )} - - ); - })} + {concept.name} + + + {isPositive ? '+' : ''}{concept.change_pct.toFixed(1)}% + ); })} - + ); }; /** - * 产品特性图标组件 - */ -const FeatureIcon = ({ icon, title, description }) => { - return ( - - - - - - - {title} - - - {description} - - - - ); -}; - -/** - * 顶部说明面板主组件 + * 顶部说明面板主组件 - 驹形克己风格 */ const HeroPanel = () => { - const gradientBg = 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 25%, #16213e 50%, #1a1a2e 75%, #0a0a0a 100%)'; - const cardBorder = '1px solid'; - const borderColor = useColorModeValue('rgba(255, 215, 0, 0.4)', 'rgba(255, 215, 0, 0.3)'); + const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); return ( - - {/* 动态网格背景 */} + {/* 主容器 - 极简玻璃态 */} + bg="rgba(15, 15, 20, 0.6)" + backdropFilter="blur(20px)" + borderRadius="2xl" + border="1px solid" + borderColor="whiteAlpha.100" + overflow="hidden" + position="relative" + > + {/* 装饰性几何元素 - 驹形克己风格 */} + {/* 右上角大圆 */} + + {/* 左下角半圆 */} + + {/* 中间小圆点 */} + + - {/* 扫描线效果 */} - - - {/* 装饰性光晕 - 多层动态叠加 */} - - - - - {/* 四角光点装饰 */} - - - - - - - - {/* 左侧:产品介绍 */} - + {/* 顶部标题行 */} + - - - - 价值前沿 - - - - 实时捕捉市场动态,智能分析投资机会。 -
- 整合多维数据源,为您提供专业的投资决策支持。 + {/* 左侧:标题 */} + + + 事件中心 - - - - - - - - - - - - - - - -
-
- - {/* 中间:沪深指数K线图 */} - - - - - - - - - - - - {/* 右侧:热门概念词云图 */} - - - - - - 🔥 热门概念 - - - + {isInTradingTime() && ( + - - 实时更新 + + 交易中 - - - - - - 🌊 概念流动展示 - - 🎨 颜色表示涨跌幅 - - 👆 点击查看详情 - - - -
-
-
+ )} + + + {/* 右侧:说明按钮 */} + + + + 使用说明 + + {isOpen ? ( + + ) : ( + + )} + + + + {/* 可折叠的说明区域 */} + + + + {/* 评级说明 */} + + + + + 关于事件评级 + + + 重要度 + + S · A · B · C + + 由大模型基于事件影响力评估,与收益率预测无关。事件包含 + 利好 + 和 + 利空 + 两类,请在「历史相关事件」中查看历史表现来判断投资价值。 + + + + + {/* 延迟提醒 */} + + + + + 延迟提醒 + + + 模型需回溯历史数据,事件结果会有 + + 2-3 分钟 + + 延迟,请 + + 不要追高 + + + + + + + + + {/* 三栏布局 */} + + {/* 左侧:上证指数 */} + + + + + {/* 中间:深证成指 */} + + + + + {/* 右侧:热门概念 */} + + + + + 热门概念 + + + 点击查看详情 + + + + + + + + + + + ); };