diff --git a/src/views/Community/components/HeroPanel/components/CalendarCell.js b/src/views/Community/components/HeroPanel/components/CalendarCell.js new file mode 100644 index 00000000..3de10ac3 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/CalendarCell.js @@ -0,0 +1,276 @@ +// HeroPanel - 日历单元格组件 +import React, { memo } from "react"; +import { + Box, + VStack, + HStack, + Text, + Badge, + Tooltip, + Icon, +} from "@chakra-ui/react"; +import { Flame, FileText, TrendingUp, TrendingDown } from "lucide-react"; +import { goldColors, textColors } from "../constants"; +import { getHeatColor } from "../utils"; + +/** + * 趋势图标 + */ +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"; + +/** + * 日历单元格 - 显示涨停数和事件数(加大尺寸) + * 新增:连续概念连接展示(connectLeft/connectRight 表示与左右格子是否同一概念) + */ +const CalendarCell = memo( + ({ + date, + ztData, + eventCount, + previousZtData, + isSelected, + isToday, + isWeekend, + onClick, + connectLeft, + connectRight, + }) => { + if (!date) { + return ; + } + + const hasZtData = !!ztData; + const hasEventData = eventCount > 0; + const ztCount = ztData?.count || 0; + const heatColors = getHeatColor(ztCount); + const topSector = ztData?.top_sector || ""; + + // 是否有连接线(连续概念) + const hasConnection = connectLeft || connectRight; + + // 周末无数据显示"休市" + if (isWeekend && !hasZtData && !hasEventData) { + return ( + + + {date.getDate()} + + + 休市 + + + ); + } + + // 正常日期 + return ( + + {`${ + date.getMonth() + 1 + }月${date.getDate()}日`} + {hasZtData && ( + + 涨停: {ztCount}家 {topSector && `| ${topSector}`} + + )} + {hasEventData && 未来事件: {eventCount}个} + {!hasZtData && !hasEventData && ( + 暂无数据 + )} + + } + placement="top" + hasArrow + bg="rgba(15, 15, 22, 0.95)" + border="1px solid rgba(212, 175, 55, 0.3)" + borderRadius="10px" + > + onClick && onClick(date)} + w="full" + minH="75px" + > + {/* 今天标记 */} + {isToday && ( + + 今天 + + )} + + + {/* 日期 */} + + {date.getDate()} + + + {/* 涨停数 + 趋势 */} + {hasZtData && ( + + + + {ztCount} + + + + )} + + {/* 事件数 */} + {hasEventData && ( + + + + {eventCount} + + + )} + + {/* 主要板块 - 连续概念用连接样式 */} + {hasZtData && topSector && ( + + {/* 左连接线 */} + {connectLeft && ( + + )} + + {topSector} + + {/* 右连接线 */} + {connectRight && ( + + )} + + )} + + + + ); + } +); + +CalendarCell.displayName = "CalendarCell"; + +export default CalendarCell; diff --git a/src/views/Community/components/HeroPanel/components/CombinedCalendar.js b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js new file mode 100644 index 00000000..b925b58f --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js @@ -0,0 +1,263 @@ +// HeroPanel - 综合日历组件 +import React, { useState, useEffect, useCallback, Suspense, lazy } from "react"; +import { + Box, + HStack, + VStack, + Text, + Icon, + Center, + Spinner, +} from "@chakra-ui/react"; +import { Flame } from "lucide-react"; +import dayjs from "dayjs"; +import { GLASS_BLUR } from "@/constants/glassConfig"; +import { eventService } from "@services/eventService"; +import { getApiBase } from "@utils/apiConfig"; +import { textColors } from "../constants"; +import { formatDateStr } from "../utils"; + +// 懒加载 FullCalendar +const FullCalendarPro = lazy(() => + import("@components/Calendar").then((module) => ({ + default: module.FullCalendarPro, + })) +); + +/** + * 综合日历组件 - 使用 FullCalendarPro 实现跨天事件条效果 + * @param {Object} props + * @param {React.ComponentType} props.DetailModal - 详情弹窗组件 + */ +const CombinedCalendar = ({ DetailModal }) => { + const [currentMonth, setCurrentMonth] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(null); + + // 日历综合数据(涨停 + 事件 + 上证涨跌幅)- 使用新的综合 API + const [calendarData, setCalendarData] = useState([]); + const [ztDailyDetails, setZtDailyDetails] = useState({}); + const [selectedZtDetail, setSelectedZtDetail] = useState(null); + const [selectedEvents, setSelectedEvents] = useState([]); + + const [detailLoading, setDetailLoading] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + + // 加载日历综合数据(一次 API 调用获取所有数据) + useEffect(() => { + const loadCalendarCombinedData = async () => { + try { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth() + 1; + const response = await fetch( + `${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}` + ); + if (response.ok) { + const result = await response.json(); + if (result.success && result.data) { + // 转换为 FullCalendarPro 需要的格式 + const formattedData = result.data.map((item) => ({ + date: item.date, + count: item.zt_count || 0, + topSector: item.top_sector || "", + eventCount: item.event_count || 0, + indexChange: item.index_change, + })); + console.log( + "[HeroPanel] 加载日历综合数据成功,数据条数:", + formattedData.length + ); + setCalendarData(formattedData); + } + } + } catch (error) { + console.error("Failed to load calendar combined data:", error); + } + }; + loadCalendarCombinedData(); + }, [currentMonth]); + + // 处理日期点击 - 打开弹窗 + const handleDateClick = useCallback( + async (date) => { + setSelectedDate(date); + setModalOpen(true); + setDetailLoading(true); + + const ztDateStr = formatDateStr(date); + const eventDateStr = dayjs(date).format("YYYY-MM-DD"); + + // 加载涨停详情 + const detail = ztDailyDetails[ztDateStr]; + if (detail?.fullData) { + setSelectedZtDetail(detail.fullData); + } else { + try { + const response = await fetch(`/data/zt/daily/${ztDateStr}.json`); + if (response.ok) { + const data = await response.json(); + setSelectedZtDetail(data); + setZtDailyDetails((prev) => ({ + ...prev, + [ztDateStr]: { ...prev[ztDateStr], fullData: data }, + })); + } else { + setSelectedZtDetail(null); + } + } catch { + setSelectedZtDetail(null); + } + } + + // 加载事件详情 + try { + const response = await eventService.calendar.getEventsForDate( + eventDateStr + ); + if (response.success) { + setSelectedEvents(response.data || []); + } else { + setSelectedEvents([]); + } + } catch { + setSelectedEvents([]); + } + + setDetailLoading(false); + }, + [ztDailyDetails] + ); + + // 月份变化回调 + const handleMonthChange = useCallback((year, month) => { + setCurrentMonth(new Date(year, month - 1, 1)); + }, []); + + return ( + <> + + {/* 顶部装饰条 */} + + + {/* 图例说明 - 右上角 */} + + + + + 热门概念 + + + + + + ≥60 + + + + + + <60 + + + + + + N + + + + 事件 + + + + + + + + + / + + + - + + + 上证 + + + + + {/* FullCalendar Pro - 炫酷跨天事件条日历(懒加载) */} + + + + + 加载日历组件... + + + + } + > + + + + + {/* 详情弹窗 */} + {DetailModal && ( + setModalOpen(false)} + selectedDate={selectedDate} + ztDetail={selectedZtDetail} + events={selectedEvents} + loading={detailLoading} + /> + )} + > + ); +}; + +export default CombinedCalendar; diff --git a/src/views/Community/components/HeroPanel/components/HotKeywordsCloud.js b/src/views/Community/components/HeroPanel/components/HotKeywordsCloud.js new file mode 100644 index 00000000..cd22d6e6 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/HotKeywordsCloud.js @@ -0,0 +1,122 @@ +// 热门关键词云组件 +// 用于涨停分析 Tab 显示今日热词 + +import React from "react"; +import { FireOutlined } from "@ant-design/icons"; + +/** + * 获取关键词样式(根据排名) + */ +const getKeywordStyle = (index) => { + if (index < 3) { + return { + fontSize: "15px", + fontWeight: "bold", + background: + "linear-gradient(135deg, rgba(255,215,0,0.3) 0%, rgba(255,165,0,0.2) 100%)", + border: "1px solid rgba(255,215,0,0.5)", + color: "#FFD700", + padding: "6px 12px", + }; + } + if (index < 6) { + return { + fontSize: "14px", + fontWeight: "600", + background: "rgba(255,215,0,0.15)", + border: "1px solid rgba(255,215,0,0.3)", + color: "#D4A84B", + padding: "4px 10px", + }; + } + return { + fontSize: "13px", + fontWeight: "normal", + background: "rgba(255,255,255,0.08)", + border: "1px solid rgba(255,255,255,0.15)", + color: "#888", + padding: "2px 8px", + }; +}; + +/** + * 热门关键词云组件 + * @param {Object} props + * @param {Array} props.keywords - 关键词数组 [{ name: string }] + */ +const HotKeywordsCloud = ({ keywords }) => { + if (!keywords || keywords.length === 0) { + return null; + } + + return ( + + {/* 装饰线 */} + + + + + + + 今日热词 + + + 词频越高排名越前 + + + + {keywords.map((kw, idx) => { + const style = getKeywordStyle(idx); + return ( + + {kw.name} + + ); + })} + + + ); +}; + +export default HotKeywordsCloud; diff --git a/src/views/Community/components/HeroPanel/components/InfoModal.js b/src/views/Community/components/HeroPanel/components/InfoModal.js new file mode 100644 index 00000000..b4df64e5 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/InfoModal.js @@ -0,0 +1,152 @@ +// HeroPanel - 使用说明弹窗组件 +import React, { useState } from "react"; +import { HStack, Icon, Text } from "@chakra-ui/react"; +import { Modal as AntModal, ConfigProvider, theme } from "antd"; +import { Info } from "lucide-react"; +import { GLASS_BLUR } from "@/constants/glassConfig"; + +/** + * 使用说明弹窗组件 + */ +const InfoModal = () => { + const [isOpen, setIsOpen] = useState(false); + const onOpen = () => setIsOpen(true); + const onClose = () => setIsOpen(false); + + return ( + <> + + + + 使用说明 + + + + + + + + + + 事件中心使用指南 + + + } + styles={{ + header: { + background: 'rgba(25,25,50,0.98)', + borderBottom: '1px solid rgba(255,215,0,0.2)', + paddingBottom: '16px', + }, + body: { + background: 'linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)', + padding: '24px', + }, + content: { + background: 'linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)', + border: '1px solid rgba(255,215,0,0.3)', + borderRadius: '16px', + boxShadow: '0 25px 80px rgba(0,0,0,0.8)', + }, + mask: { + background: 'rgba(0,0,0,0.7)', + backdropFilter: GLASS_BLUR.sm, + }, + }} + > + + + + 📅 综合日历 + + + 日历同时展示 + 历史涨停数据 + 和 + 未来事件 + , 点击日期查看详细信息。 + + + + + + 🔥 涨停板块 + + + 点击历史日期,查看当日涨停板块排行、涨停数量、涨停股票代码,帮助理解市场主线。 + + + + + + 📊 未来事件 + + + 点击未来日期,查看事件详情,包括 + 背景分析 + 、 + 未来推演 + 、 + 相关股票 + 等。 + + + + + + 💡 颜色越深表示涨停数越多 · 绿色标记表示有未来事件 + + + + + + > + ); +}; + +export default InfoModal; diff --git a/src/views/Community/components/HeroPanel/components/ZTStatsCards.js b/src/views/Community/components/HeroPanel/components/ZTStatsCards.js new file mode 100644 index 00000000..2a6950d0 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/ZTStatsCards.js @@ -0,0 +1,171 @@ +// 涨停统计卡片组件 +// 显示连板分布、封板时间、公告驱动统计 + +import React from "react"; + +/** + * 获取连板颜色 + */ +const getContinuousColor = (key) => { + if (key === "4连板+") return "#ff4d4f"; + if (key === "3连板") return "#fa541c"; + if (key === "2连板") return "#fa8c16"; + return "#52c41a"; +}; + +/** + * 获取时间颜色 + */ +const getTimeColor = (key) => { + if (key === "秒板") return "#ff4d4f"; + if (key === "早盘") return "#fa8c16"; + if (key === "盘中") return "#52c41a"; + return "#888"; +}; + +/** + * 统计卡片基础样式 + */ +const cardStyle = { + flex: 1, + minWidth: "200px", + padding: "12px", + background: "rgba(255,255,255,0.03)", + borderRadius: "12px", + border: "1px solid rgba(255,255,255,0.08)", +}; + +/** + * 涨停统计卡片组件 + * @param {Object} props + * @param {Object} props.stats - 统计数据 + * @param {Object} props.stats.continuousStats - 连板分布 + * @param {Object} props.stats.timeStats - 时间分布 + * @param {number} props.stats.announcementCount - 公告驱动数 + * @param {number} props.stats.announcementRatio - 公告驱动占比 + */ +const ZTStatsCards = ({ stats }) => { + if (!stats) { + return null; + } + + return ( + + {/* 连板分布 */} + + + 连板分布 + + + {Object.entries(stats.continuousStats).map(([key, value]) => ( + + + {value} + + + {key} + + + ))} + + + + {/* 涨停时间分布 */} + + + 封板时间 + + + {Object.entries(stats.timeStats).map(([key, value]) => ( + + + {value} + + + {key} + + + ))} + + + + {/* 公告驱动 */} + + + 公告驱动 + + + + {stats.announcementCount} + + + 只 ({stats.announcementRatio}%) + + + + + ); +}; + +export default ZTStatsCards; diff --git a/src/views/Community/components/HeroPanel/components/index.js b/src/views/Community/components/HeroPanel/components/index.js new file mode 100644 index 00000000..8de31be0 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/index.js @@ -0,0 +1,7 @@ +// HeroPanel 子组件导出 +export * from "./DetailModal"; +export { default as CalendarCell } from "./CalendarCell"; +export { default as InfoModal } from "./InfoModal"; +export { default as CombinedCalendar } from "./CombinedCalendar"; +export { default as HotKeywordsCloud } from "./HotKeywordsCloud"; +export { default as ZTStatsCards } from "./ZTStatsCards";