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 = () => {
)}
-
- {/* 右侧:紧凑型日历 */}
-
+
+ {/* 涨停日历 */}
+
);