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() && (
-
-
-
- 交易中
-
-
- )}
-
-
- {/* 内容区:指数 + 概念 */}
-
- {/* 左侧:双指数横向排列 */}
-
- {/* 上证指数 */}
-
-
-
-
- {/* 深证成指 */}
-
-
-
-
-
- {/* 右侧:热门概念 - 流动式设计 */}
-
- {/* 标题栏 - 更精致的设计 */}
-
-
-
-
-
-
-
- 热门概念
-
-
- 实时涨跌排行
-
-
-
-
-
- {/* 流动式概念展示 */}
-
-
-
-
+ {/* 右侧:紧凑型日历 */}
+