From 28b1085b8a5187b6d6b8272b6285fab1c8cdfd93 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Wed, 7 Jan 2026 11:08:04 +0800 Subject: [PATCH] =?UTF-8?q?community=E5=A2=9E=E5=8A=A0=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Community/components/HeroPanel.js | 941 ++++++-------------- 1 file changed, 295 insertions(+), 646 deletions(-) diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index aee424f1..a2358978 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -1,8 +1,8 @@ // src/views/Community/components/HeroPanel.js -// 顶部说明面板组件:事件中心 + 沪深指数K线图 + 热门概念3D动画 -// 交易时间内自动更新指数行情(每分钟一次) +// 顶部说明面板组件:事件中心标题 + 投资日历 +// 简化版本:移除了指数K线和热门概念,由右侧边栏提供 -import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { Box, Card, @@ -24,15 +24,17 @@ import { ModalBody, ModalCloseButton, Tooltip, + Badge, + Grid, + GridItem, + IconButton, } from '@chakra-ui/react'; -import { AlertCircle, Clock, TrendingUp, Info, RefreshCw } from 'lucide-react'; -import ReactECharts from 'echarts-for-react'; +import { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight } from 'lucide-react'; +import dayjs from 'dayjs'; import { logger } from '@utils/logger'; import { getApiBase } from '@utils/apiConfig'; -import { getConceptHtmlUrl } from '@utils/textUtils'; -import { useIndexQuote } from '@hooks/useIndexQuote'; -import conceptStaticService from '@services/conceptStaticService'; import { GLASS_BLUR } from '@/constants/glassConfig'; +import InvestmentCalendar from '@components/InvestmentCalendar'; // 定义动画 const animations = ` @@ -44,10 +46,6 @@ const animations = ` 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } - @keyframes floatSlow { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-10px); } - } `; // 注入样式 @@ -61,40 +59,6 @@ if (typeof document !== 'undefined') { } } -/** - * 获取指数行情数据(日线数据) - */ -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 []; - } -}; - /** * 判断当前是否在交易时间内 */ @@ -105,501 +69,294 @@ const isInTradingTime = () => { }; /** - * 精美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 ` -
-
- 📅 ${raw.time || '-'} -
-
- 开盘 - ${safeFixed(raw.open)} - 收盘 - ${safeFixed(raw.close)} - 最高 - ${safeFixed(raw.high)} - 最低 - ${safeFixed(raw.low)} -
-
- 涨跌幅 - - ${sign}${safeFixed(changeAmount)} (${sign}${safeFixed(changePct)}%) - -
-
- `; - } - }, - xAxis: [ - { - type: 'category', - data: chartData.dates, - gridIndex: 0, - show: false, - boundaryGap: true - }, - { - type: 'category', - data: chartData.dates, - gridIndex: 1, - show: false, - boundaryGap: true - } - ], - yAxis: [ - { - type: 'value', - gridIndex: 0, - show: false, - scale: true - }, - { - type: 'value', - gridIndex: 1, - show: false, - scale: true - } - ], - dataZoom: [{ - type: 'inside', - xAxisIndex: [0, 1], - start: 50, - end: 100, - zoomOnMouseWheel: true, - moveOnMouseMove: true - }], - series: [ - { - name: 'K线', - type: 'candlestick', - data: chartData.klineData, - xAxisIndex: 0, - yAxisIndex: 0, - itemStyle: { - color: upColor, - color0: downColor, - borderColor: upColor, - borderColor0: downColor, - borderWidth: 1 - }, - barWidth: '65%' - }, - { - name: '成交量', - type: 'bar', - data: chartData.volumes, - xAxisIndex: 1, - yAxisIndex: 1, - itemStyle: { - color: (params) => { - const idx = params.dataIndex; - const raw = chartData.rawData[idx]; - return raw && raw.close >= raw.open ? 'rgba(239,83,80,0.5)' : 'rgba(38,166,154,0.5)'; - } - }, - barWidth: '65%' - } - ] - }; - }, [chartData, upColor, downColor]); - - if (loading) { - return ( -
- - - 加载{indexName}... - -
- ); +const fetchEventCounts = async (year, month) => { + try { + const response = await fetch(`${getApiBase()}/api/events/calendar/counts?year=${year}&month=${month}`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + const data = await response.json(); + return data.success ? data.data : []; + } catch (error) { + logger.error('HeroPanel', 'fetchEventCounts error', { year, month, error: error.message }); + return []; } - - return ( - - {/* 顶部:指数名称和数据 */} - - - - - {indexName} - - {/* 实时状态指示 */} - {isTrading && latestData?.isRealtime && ( - - - - - 实时 - - - - )} - - - - {latestData?.close?.toFixed(2)} - - - - {latestData?.isPositive ? '▲' : '▼'} {latestData?.isPositive ? '+' : ''}{latestData?.changePct?.toFixed(2)}% - - - - - - {/* K线图区域 */} - - - {/* 底部提示 - 显示更新时间 */} - - {latestData?.updateTime && ( - - {latestData.updateTime} - - )} - - 滚轮缩放 · 拖动查看 - - - - - ); }; /** - * 流动式热门概念组件 - HeroUI 风格 + * 紧凑型投资日历组件 - 深色主题风格 * 特点: - * 1. 三行横向滚动,每行方向不同 - * 2. 卡片式设计,带渐变边框 - * 3. 悬停时暂停滚动,放大效果 - * 4. 流光动画效果 + * 1. 紧凑的月历视图 + * 2. 事件数量用圆点颜色深浅表示 + * 3. 悬停显示当日事件数 + * 4. 点击打开完整日历弹窗 */ -const FlowingConcepts = () => { - const [concepts, setConcepts] = useState([]); +const CompactCalendar = () => { + const [currentMonth, setCurrentMonth] = useState(dayjs()); + const [eventCounts, setEventCounts] = useState([]); const [loading, setLoading] = useState(true); - const [hoveredIdx, setHoveredIdx] = useState(null); - const [isPaused, setIsPaused] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + + // 加载月度事件统计 + const loadEventCounts = useCallback(async () => { + setLoading(true); + const data = await fetchEventCounts(currentMonth.year(), currentMonth.month() + 1); + setEventCounts(data); + setLoading(false); + }, [currentMonth]); useEffect(() => { - const load = async () => { - const data = await fetchPopularConcepts(); - setConcepts(data.slice(0, 30)); // 取30个概念 - setLoading(false); - }; - load(); - }, []); + loadEventCounts(); + }, [loadEventCounts]); - const getColor = (pct) => { - if (pct > 5) return { bg: 'rgba(255,23,68,0.15)', border: '#ff1744', text: '#ff1744', glow: 'rgba(255,23,68,0.4)' }; - if (pct > 2) return { bg: 'rgba(255,82,82,0.12)', border: '#ff5252', text: '#ff5252', glow: 'rgba(255,82,82,0.3)' }; - if (pct > 0) return { bg: 'rgba(255,138,128,0.1)', border: '#ff8a80', text: '#ff8a80', glow: 'rgba(255,138,128,0.25)' }; - if (pct === 0) return { bg: 'rgba(255,215,0,0.1)', border: '#FFD700', text: '#FFD700', glow: 'rgba(255,215,0,0.25)' }; - if (pct > -2) return { bg: 'rgba(105,240,174,0.1)', border: '#69f0ae', text: '#69f0ae', glow: 'rgba(105,240,174,0.25)' }; - if (pct > -5) return { bg: 'rgba(0,230,118,0.12)', border: '#00e676', text: '#00e676', glow: 'rgba(0,230,118,0.3)' }; - return { bg: 'rgba(0,200,83,0.15)', border: '#00c853', text: '#00c853', glow: 'rgba(0,200,83,0.4)' }; + // 获取事件数量映射 + const getEventCountForDate = (dateStr) => { + const found = eventCounts.find(item => item.date === dateStr); + return found ? found.count : 0; }; - const handleClick = (name) => { - window.open(getConceptHtmlUrl(name), '_blank'); + // 根据事件数量获取颜色 + const getEventColor = (count) => { + if (count === 0) return 'transparent'; + if (count >= 15) return 'rgba(239, 68, 68, 0.9)'; // 深红 + if (count >= 10) return 'rgba(249, 115, 22, 0.85)'; // 橙红 + if (count >= 5) return 'rgba(234, 179, 8, 0.8)'; // 金黄 + if (count >= 3) return 'rgba(34, 197, 94, 0.75)'; // 绿色 + return 'rgba(59, 130, 246, 0.7)'; // 蓝色 }; - if (loading) { - return ( -
- - - 加载热门概念... - -
- ); - } + // 生成日历网格 + const generateCalendarDays = () => { + const startOfMonth = currentMonth.startOf('month'); + const endOfMonth = currentMonth.endOf('month'); + const startDay = startOfMonth.day(); // 0=周日 + const daysInMonth = endOfMonth.date(); - // 将概念分成三行 - const row1 = concepts.slice(0, 10); - const row2 = concepts.slice(10, 20); - const row3 = concepts.slice(20, 30); + const days = []; + const today = dayjs().format('YYYY-MM-DD'); - // 渲染单个概念卡片 - const renderConceptCard = (concept, globalIdx, uniqueIdx) => { - const colors = getColor(concept.change_pct); - const isActive = hoveredIdx === globalIdx; + // 填充月初空白 + for (let i = 0; i < startDay; i++) { + days.push({ date: null, day: null }); + } - return ( + // 填充日期 + for (let day = 1; day <= daysInMonth; day++) { + const date = currentMonth.date(day).format('YYYY-MM-DD'); + const count = getEventCountForDate(date); + days.push({ + date, + day, + count, + isToday: date === today, + }); + } + + return days; + }; + + const days = generateCalendarDays(); + const weekDays = ['日', '一', '二', '三', '四', '五', '六']; + + return ( + <> handleClick(concept.name)} - onMouseEnter={() => { - setHoveredIdx(globalIdx); - setIsPaused(true); - }} - onMouseLeave={() => { - setHoveredIdx(null); - setIsPaused(false); - }} + border="1px solid rgba(255,215,0,0.2)" + p={3} + minW="280px" position="relative" overflow="hidden" - _before={isActive ? { - content: '""', - position: 'absolute', - top: 0, - left: '-100%', - width: '200%', - height: '100%', - background: `linear-gradient(90deg, transparent, ${colors.glow}, transparent)`, - animation: 'shimmer 1.5s infinite', - } : {}} - > - - - {concept.name} - - - {concept.change_pct > 0 ? '+' : ''}{concept.change_pct?.toFixed(2) ?? '-'}% - - - - ); - }; - - // 渲染滚动行 - const renderScrollRow = (items, direction, startIdx, duration) => { - const animationName = direction === 'left' ? 'scrollLeft' : 'scrollRight'; - - return ( - - - {/* 复制两份实现无缝滚动 */} - {[...items, ...items].map((concept, idx) => - renderConceptCard(concept, startIdx + (idx % items.length), idx) - )} + {/* 头部:月份切换 */} + + + + + + + 投资日历 + + + + } + size="xs" + variant="ghost" + color="whiteAlpha.700" + _hover={{ bg: 'whiteAlpha.100', color: 'white' }} + onClick={() => setCurrentMonth(prev => prev.subtract(1, 'month'))} + aria-label="上个月" + /> + + {currentMonth.format('YYYY年M月')} + + } + size="xs" + variant="ghost" + color="whiteAlpha.700" + _hover={{ bg: 'whiteAlpha.100', color: 'white' }} + onClick={() => setCurrentMonth(prev => prev.add(1, 'month'))} + aria-label="下个月" + /> + + + + {/* 星期头 */} + + {weekDays.map((day, idx) => ( + + + {day} + + + ))} + + + {/* 日期网格 */} + {loading ? ( +
+ +
+ ) : ( + + {days.map((item, idx) => ( + + {item.day ? ( + 0 ? `${item.count}个事件` : '无事件'} + placement="top" + hasArrow + bg="gray.800" + color="white" + fontSize="xs" + > + setIsModalOpen(true)} + > + + {item.day} + + {/* 事件指示点 */} + {item.count > 0 && ( + + )} + + + ) : ( + + )} + + ))} + + )} + + {/* 底部:查看更多 */} + + setIsModalOpen(true)} + > + 点击查看详细日历 → +
- ); - }; - return ( - - - {renderScrollRow(row1, 'left', 0, 35)} - {renderScrollRow(row2, 'right', 10, 40)} - {renderScrollRow(row3, 'left', 20, 32)} - - + {/* 完整日历弹窗 */} + setIsModalOpen(false)} + size="6xl" + > + + + + + + + + + 投资日历 + + + + + + + + + + ); }; @@ -810,9 +567,10 @@ const HeroPanel = () => { filter="blur(50px)" /> - - {/* 标题行:标题 + 使用说明 + 交易状态 */} - + + {/* 标题行:标题 + 使用说明 + 日历 + 交易状态 */} + + {/* 左侧:标题 + 使用说明 */} { {/* 使用说明 - 弹窗 */} + {/* 交易状态 */} + {isInTradingTime() && ( + + + + 交易中 + + + )} - {/* 右侧:交易状态 */} - {isInTradingTime() && ( - - - - 交易中 - - - )} - - - {/* 内容区:指数 + 概念 */} - - {/* 左侧:双指数横向排列 */} - - {/* 上证指数 */} - - - - - {/* 深证成指 */} - - - - - - {/* 右侧:热门概念 - 流动式设计 */} - - {/* 标题栏 - 更精致的设计 */} - - - - - - - - 热门概念 - - - 实时涨跌排行 - - - - - - {/* 流动式概念展示 */} - - - - + {/* 右侧:紧凑型日历 */} +