diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index a2358978..4471494f 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线和热门概念,由右侧边栏提供 +// 顶部说明面板组件:事件中心标题 + 涨停日历 +// 大尺寸日历,显示每日涨停数和主要板块 -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useMemo, memo } from 'react'; import { Box, Card, @@ -25,16 +25,11 @@ import { ModalCloseButton, Tooltip, Badge, - Grid, - GridItem, + SimpleGrid, IconButton, } from '@chakra-ui/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 { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight, Flame, TrendingUp, TrendingDown } from 'lucide-react'; import { GLASS_BLUR } from '@/constants/glassConfig'; -import InvestmentCalendar from '@components/InvestmentCalendar'; // 定义动画 const animations = ` @@ -68,295 +63,466 @@ const isInTradingTime = () => { return timeInMinutes >= 570 && timeInMinutes <= 900; }; -/** - * 获取月度事件统计 - */ -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 []; - } +// 主题色配置 +const goldColors = { + primary: '#D4AF37', + light: '#F4D03F', + dark: '#B8860B', + glow: 'rgba(212, 175, 55, 0.4)', }; +const textColors = { + primary: '#ffffff', + secondary: 'rgba(255, 255, 255, 0.85)', + muted: 'rgba(255, 255, 255, 0.5)', +}; + +// 热度级别配置 +const HEAT_LEVELS = [ + { key: 'high', threshold: 80, colors: { bg: 'rgba(147, 51, 234, 0.55)', text: '#d8b4fe', border: 'rgba(147, 51, 234, 0.65)' } }, + { key: 'medium', threshold: 60, colors: { bg: 'rgba(239, 68, 68, 0.50)', text: '#fca5a5', border: 'rgba(239, 68, 68, 0.60)' } }, + { key: 'low', threshold: 40, colors: { bg: 'rgba(251, 146, 60, 0.45)', text: '#fed7aa', border: 'rgba(251, 146, 60, 0.55)' } }, + { key: 'cold', threshold: 0, colors: { bg: 'rgba(59, 130, 246, 0.35)', text: '#93c5fd', border: 'rgba(59, 130, 246, 0.45)' } }, +]; + +const DEFAULT_HEAT_COLORS = { + bg: 'rgba(60, 60, 70, 0.12)', + text: textColors.muted, + border: 'transparent', +}; + +const getHeatColor = (count) => { + if (!count) return DEFAULT_HEAT_COLORS; + const level = HEAT_LEVELS.find((l) => count >= l.threshold); + return level?.colors || DEFAULT_HEAT_COLORS; +}; + +// 日期格式化 +const formatDateStr = (date) => { + if (!date) return ''; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}${month}${day}`; +}; + +const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六']; +const MONTH_NAMES = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; + /** - * 紧凑型投资日历组件 - 深色主题风格 - * 特点: - * 1. 紧凑的月历视图 - * 2. 事件数量用圆点颜色深浅表示 - * 3. 悬停显示当日事件数 - * 4. 点击打开完整日历弹窗 + * 趋势图标 */ -const CompactCalendar = () => { - const [currentMonth, setCurrentMonth] = useState(dayjs()); - const [eventCounts, setEventCounts] = useState([]); - const [loading, setLoading] = useState(true); - 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(() => { - loadEventCounts(); - }, [loadEventCounts]); - - // 获取事件数量映射 - const getEventCountForDate = (dateStr) => { - const found = eventCounts.find(item => item.date === dateStr); - return found ? found.count : 0; - }; - - // 根据事件数量获取颜色 - 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)'; // 蓝色 - }; - - // 生成日历网格 - const generateCalendarDays = () => { - const startOfMonth = currentMonth.startOf('month'); - const endOfMonth = currentMonth.endOf('month'); - const startDay = startOfMonth.day(); // 0=周日 - const daysInMonth = endOfMonth.date(); - - const days = []; - const today = dayjs().format('YYYY-MM-DD'); - - // 填充月初空白 - for (let i = 0; i < startDay; i++) { - days.push({ date: null, day: null }); - } - - // 填充日期 - 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 = ['日', '一', '二', '三', '四', '五', '六']; +const TrendIcon = memo(({ current, previous }) => { + if (!current || !previous) return null; + const diff = current - previous; + if (diff === 0) return null; + const isUp = diff > 0; return ( - <> + + ); +}); + +TrendIcon.displayName = 'TrendIcon'; + +/** + * 日历单元格 + */ +const CalendarCell = memo(({ date, dateData, previousData, isSelected, isToday, isWeekend, onClick }) => { + if (!date) { + return ; + } + + const hasData = !!dateData; + const count = dateData?.count || 0; + const heatColors = getHeatColor(count); + const topSector = dateData?.top_sector || ''; + + // 周末无数据显示"休市" + if (isWeekend && !hasData) { + return ( - {/* 头部:月份切换 */} - - - - - - - 投资日历 - - - - } - 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="下个月" - /> - - + + {date.getDate()} + + + 休市 + + + ); + } - {/* 星期头 */} - - {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 && ( - - )} - - - ) : ( - - )} - - ))} - + // 正常日期 + return ( + + {`${date.getMonth() + 1}月${date.getDate()}日`} + {hasData ? ( + <> + 涨停数: {count}家 + {topSector && 主线板块: {topSector}} + + ) : ( + 暂无数据 + )} + + } + placement="top" + hasArrow + bg="rgba(15, 15, 22, 0.95)" + border="1px solid rgba(212, 175, 55, 0.3)" + borderRadius="12px" + > + onClick && onClick(date)} + w="full" + minH="70px" + > + {/* 今天标记 */} + {isToday && ( + + 今天 + )} - {/* 底部:查看更多 */} - + + {/* 日期 */} setIsModalOpen(true)} + fontSize="md" + fontWeight={isSelected || isToday ? 'bold' : '500'} + color={isSelected ? goldColors.primary : isToday ? goldColors.light : textColors.primary} > - 点击查看详细日历 → + {date.getDate()} - - - {/* 完整日历弹窗 */} - setIsModalOpen(false)} - size="6xl" - > - - - - - - - - - 投资日历 + {/* 涨停数 + 趋势 */} + {hasData && ( + + + {count}家 + + + + )} + + {/* 主要板块 */} + {hasData && topSector && ( + + {count >= 80 && } + + {topSector} - - - - - - - - + )} + +
+ + ); +}); + +CalendarCell.displayName = 'CalendarCell'; + +/** + * 涨停日历组件 - 大尺寸版本 + */ +const LimitUpCalendar = () => { + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [datesData, setDatesData] = useState([]); + const [dailyDetails, setDailyDetails] = useState({}); + const [loading, setLoading] = useState(true); + + // 加载 dates.json + useEffect(() => { + const loadDatesData = async () => { + try { + const response = await fetch('/data/zt/dates.json'); + if (response.ok) { + const data = await response.json(); + setDatesData(data.dates || []); + } + } catch (error) { + console.error('Failed to load dates.json:', error); + } finally { + setLoading(false); + } + }; + loadDatesData(); + }, []); + + // 获取当月需要的日期详情(主要板块) + useEffect(() => { + const loadMonthDetails = async () => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + const details = {}; + const promises = []; + + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${year}${String(month + 1).padStart(2, '0')}${String(day).padStart(2, '0')}`; + // 检查是否有这天的数据 + const hasData = datesData.some(d => d.date === dateStr); + if (hasData && !dailyDetails[dateStr]) { + promises.push( + fetch(`/data/zt/daily/${dateStr}.json`) + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data && data.sector_data) { + // 找出涨停数最多的板块 + let maxSector = ''; + let maxCount = 0; + Object.entries(data.sector_data).forEach(([sector, info]) => { + if (info.count > maxCount) { + maxCount = info.count; + maxSector = sector; + } + }); + details[dateStr] = { top_sector: maxSector }; + } + }) + .catch(() => null) + ); + } + } + + if (promises.length > 0) { + await Promise.all(promises); + setDailyDetails(prev => ({ ...prev, ...details })); + } + }; + + if (datesData.length > 0) { + loadMonthDetails(); + } + }, [currentMonth, datesData]); + + // 构建日期数据映射 + const dateDataMap = useMemo(() => { + const map = new Map(); + datesData.forEach(d => { + const detail = dailyDetails[d.date] || {}; + map.set(d.date, { + date: d.date, + count: d.count, + top_sector: detail.top_sector, + }); + }); + return map; + }, [datesData, dailyDetails]); + + // 生成日历天数 + const days = useMemo(() => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const daysInMonth = lastDay.getDate(); + const startingDayOfWeek = firstDay.getDay(); + + const result = []; + // 前置空白天数 + for (let i = 0; i < startingDayOfWeek; i++) { + result.push(null); + } + // 当月天数 + for (let i = 1; i <= daysInMonth; i++) { + result.push(new Date(year, month, i)); + } + return result; + }, [currentMonth]); + + // 预计算日历格子数据 + const calendarCellsData = useMemo(() => { + const today = new Date(); + const todayStr = today.toDateString(); + + return days.map((date, index) => { + if (!date) { + return { key: `empty-${index}`, date: null }; + } + + const dateStr = formatDateStr(date); + const dateData = dateDataMap.get(dateStr) || null; + const dayOfWeek = date.getDay(); + + // 获取前一天数据 + const prevDate = new Date(date); + prevDate.setDate(prevDate.getDate() - 1); + const previousData = dateDataMap.get(formatDateStr(prevDate)) || null; + + return { + key: dateStr || `day-${index}`, + date, + dateData, + previousData, + isToday: date.toDateString() === todayStr, + isWeekend: dayOfWeek === 0 || dayOfWeek === 6, + }; + }); + }, [days, dateDataMap]); + + // 月份导航 + const handlePrevMonth = useCallback(() => { + setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() - 1)); + }, []); + + const handleNextMonth = useCallback(() => { + setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() + 1)); + }, []); + + return ( + + {/* 顶部装饰条 */} + + + {/* 月份导航 */} + + } + variant="ghost" + size="sm" + color={textColors.secondary} + _hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }} + onClick={handlePrevMonth} + aria-label="上个月" + /> + + + + {currentMonth.getFullYear()}年{MONTH_NAMES[currentMonth.getMonth()]} + + + 涨停日历 + + + } + variant="ghost" + size="sm" + color={textColors.secondary} + _hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }} + onClick={handleNextMonth} + aria-label="下个月" + /> + + + {/* 星期标题 */} + + {WEEK_DAYS.map((day, idx) => ( + + {day} + + ))} + + + {/* 日历格子 */} + {loading ? ( +
+ +
+ ) : ( + + {calendarCellsData.map((cellData) => ( + + ))} + + )} + + {/* 图例 */} + + {[ + { label: '超高潮 ≥80', color: 'rgba(147, 51, 234, 0.7)' }, + { label: '高潮 ≥60', color: 'rgba(239, 68, 68, 0.65)' }, + { label: '温和 ≥40', color: 'rgba(251, 146, 60, 0.6)' }, + { label: '偏冷 <40', color: 'rgba(59, 130, 246, 0.5)' }, + ].map(({ label, color }) => ( + + + {label} + + ))} + +
); }; @@ -567,10 +733,9 @@ const HeroPanel = () => { filter="blur(50px)" /> - - {/* 标题行:标题 + 使用说明 + 日历 + 交易状态 */} - - {/* 左侧:标题 + 使用说明 */} + + {/* 标题行 */} + { 事件中心 - {/* 使用说明 - 弹窗 */} {/* 交易状态 */} {isInTradingTime() && ( @@ -609,10 +773,10 @@ const HeroPanel = () => { )} - - {/* 右侧:紧凑型日历 */} - + + {/* 涨停日历 */} + );