From 2948f149046ddfd0d4615604363d3ca192e8d1d0 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 13 Jan 2026 19:00:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor(HeroPanel):=20=E6=8F=90=E5=8F=96=20Det?= =?UTF-8?q?ailModal=20=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主弹窗 DetailModal 使用 Hook 管理状态 - ZTSectorView/ZTStockListView 使用 memo 优化 - EventsTabView 添加空状态处理 - RelatedEventsModal 涨停归因详情 - SectorStocksModal 板块股票详情 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../components/DetailModal/DetailModal.js | 1130 +++++++++++++++++ .../components/DetailModal/EventsTabView.js | 51 + .../DetailModal/RelatedEventsModal.js | 220 ++++ .../DetailModal/SectorStocksModal.js | 373 ++++++ .../components/DetailModal/ZTSectorView.js | 39 + .../components/DetailModal/ZTStockListView.js | 134 ++ .../HeroPanel/components/DetailModal/index.js | 7 + 7 files changed, 1954 insertions(+) create mode 100644 src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js create mode 100644 src/views/Community/components/HeroPanel/components/DetailModal/EventsTabView.js create mode 100644 src/views/Community/components/HeroPanel/components/DetailModal/RelatedEventsModal.js create mode 100644 src/views/Community/components/HeroPanel/components/DetailModal/SectorStocksModal.js create mode 100644 src/views/Community/components/HeroPanel/components/DetailModal/ZTSectorView.js create mode 100644 src/views/Community/components/HeroPanel/components/DetailModal/ZTStockListView.js create mode 100644 src/views/Community/components/HeroPanel/components/DetailModal/index.js diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js b/src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js new file mode 100644 index 00000000..fb82f6e5 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js @@ -0,0 +1,1130 @@ +// HeroPanel - 详情弹窗组件 +// 完整展示涨停分析和事件详情 +import React, { useCallback, useMemo } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { toggleWatchlist } from "@store/slices/stockSlice"; +import { useDetailModalState } from "../../hooks"; +import { Center } from "@chakra-ui/react"; +import { + Table, + Tabs, + Tag, + Button, + Spin, + Typography, + message, + Modal as AntModal, + ConfigProvider, + theme, +} from "antd"; +import { + CalendarOutlined, + StockOutlined, + TagsOutlined, + FireOutlined, +} from "@ant-design/icons"; +import { Calendar } from "lucide-react"; +import { GLASS_BLUR } from "@/constants/glassConfig"; +import { getApiBase } from "@utils/apiConfig"; +import ReactMarkdown from "react-markdown"; +import KLineChartModal from "@components/StockChart/KLineChartModal"; +import { getSixDigitCode, addExchangeSuffix } from "../../utils"; +import { + RelatedEventsModal, + SectorStocksModal, + ZTSectorView, + EventsTabView, + HotKeywordsCloud, + ZTStatsCards, +} from "../index"; +import { + createStockColumns, + createSectorColumns, + createZtStockColumns, + createEventColumns, +} from "../../columns"; + +const { TabPane } = Tabs; +const { Text: AntText } = Typography; + +/** + * 详情弹窗组件 - 完整展示涨停分析和事件详情 + */ +const DetailModal = ({ + isOpen, + onClose, + selectedDate, + ztDetail, + events, + loading, +}) => { + const dispatch = useDispatch(); + const reduxWatchlist = useSelector((state) => state.stock.watchlist); + + // 使用状态管理 Hook(整合 17 个状态) + const { + // UI 状态 + setters + ztViewMode, + setZtViewMode, + selectedSectorFilter, + setSelectedSectorFilter, + expandedReasons, + setExpandedReasons, + // 弹窗状态 + setters + detailDrawerVisible, + setDetailDrawerVisible, + selectedContent, + setSelectedContent, + sectorStocksModalVisible, + setSectorStocksModalVisible, + selectedSectorInfo, + setSelectedSectorInfo, + stocksDrawerVisible, + setStocksDrawerVisible, + selectedEventStocks, + setSelectedEventStocks, + selectedEventTime, + setSelectedEventTime, + selectedEventTitle, + setSelectedEventTitle, + klineModalVisible, + setKlineModalVisible, + selectedKlineStock, + setSelectedKlineStock, + relatedEventsModalVisible, + setRelatedEventsModalVisible, + selectedRelatedEvents, + setSelectedRelatedEvents, + // 数据状态 + setters + stockQuotes, + setStockQuotes, + stockQuotesLoading, + setStockQuotesLoading, + } = useDetailModalState(); + + // 板块数据处理 - 必须在条件返回之前调用所有hooks + const sectorList = useMemo(() => { + if (!ztDetail?.sector_data) return []; + return Object.entries(ztDetail.sector_data) + .filter(([name]) => name !== "其他") + .map(([name, data]) => ({ + name, + count: data.count, + stocks: data.stock_codes || [], + // 新增:关联事件数据(涨停归因) + related_events: data.related_events || [], + })) + .sort((a, b) => b.count - a.count); + }, [ztDetail]); + + // 股票详情数据处理 - 支持两种字段名:stocks 和 stock_infos + // 按连板天数降序排列(高连板在前) + const stockList = useMemo(() => { + const stocksData = ztDetail?.stocks || ztDetail?.stock_infos; + if (!stocksData) return []; + + // 解析连板天数的辅助函数 + const parseContinuousDays = (text) => { + if (!text || text === "首板") return 1; + const match = text.match(/(\d+)/); + return match ? parseInt(match[1]) : 1; + }; + + return stocksData + .map((stock) => ({ + ...stock, + key: stock.scode, + _continuousDays: parseContinuousDays(stock.continuous_days), // 用于排序 + })) + .sort((a, b) => b._continuousDays - a._continuousDays); // 降序排列 + }, [ztDetail]); + + // 筛选后的股票列表(按板块筛选) + const filteredStockList = useMemo(() => { + if (!selectedSectorFilter) return stockList; + // 根据选中板块筛选 + const sectorData = ztDetail?.sector_data?.[selectedSectorFilter]; + if (!sectorData?.stock_codes) return stockList; + const sectorStockCodes = new Set(sectorData.stock_codes); + return stockList.filter((stock) => sectorStockCodes.has(stock.scode)); + }, [stockList, selectedSectorFilter, ztDetail]); + + // 热门关键词 + const hotKeywords = useMemo(() => { + if (!ztDetail?.word_freq_data) return []; + return ztDetail.word_freq_data.slice(0, 12); + }, [ztDetail]); + + // 涨停统计数据 + const ztStats = useMemo(() => { + if (!stockList.length) return null; + + // 连板分布统计 + const continuousStats = { 首板: 0, "2连板": 0, "3连板": 0, "4连板+": 0 }; + // 涨停时间分布统计 + const timeStats = { 秒板: 0, 早盘: 0, 盘中: 0, 尾盘: 0 }; + // 公告驱动统计 + let announcementCount = 0; + + stockList.forEach((stock) => { + // 连板统计 + const days = stock.continuous_days || "首板"; + if (days === "首板" || days.includes("1")) { + continuousStats["首板"]++; + } else { + const match = days.match(/(\d+)/); + const num = match ? parseInt(match[1]) : 1; + if (num === 2) continuousStats["2连板"]++; + else if (num === 3) continuousStats["3连板"]++; + else if (num >= 4) continuousStats["4连板+"]++; + else continuousStats["首板"]++; + } + + // 时间统计 + const time = stock.formatted_time || "15:00:00"; + if (time <= "09:30:00") timeStats["秒板"]++; + else if (time <= "10:00:00") timeStats["早盘"]++; + else if (time <= "14:00:00") timeStats["盘中"]++; + else timeStats["尾盘"]++; + + // 公告驱动 + if (stock.is_announcement) announcementCount++; + }); + + return { + total: stockList.length, + continuousStats, + timeStats, + announcementCount, + announcementRatio: + stockList.length > 0 + ? Math.round((announcementCount / stockList.length) * 100) + : 0, + }; + }, [stockList]); + + // 检查股票是否已在自选中 - 必须在条件返回之前 + const isStockInWatchlist = useCallback( + (stockCode) => { + const sixDigitCode = getSixDigitCode(stockCode); + return reduxWatchlist?.some( + (item) => getSixDigitCode(item.stock_code) === sixDigitCode + ); + }, + [reduxWatchlist] + ); + + // 显示内容详情 + const showContentDetail = useCallback( + (content, title) => { + setSelectedContent({ content, title }); + setDetailDrawerVisible(true); + }, + [setSelectedContent, setDetailDrawerVisible] + ); + + // 显示K线图 + const showKline = useCallback( + (stock) => { + const code = stock.code; + const name = stock.name; + const stockCode = addExchangeSuffix(code); + setSelectedKlineStock({ + stock_code: stockCode, + stock_name: name, + }); + setKlineModalVisible(true); + }, + [setSelectedKlineStock, setKlineModalVisible] + ); + + // 添加单只股票到自选 + const addSingleToWatchlist = useCallback( + async (stock) => { + const code = stock.code; + const name = stock.name; + const stockCode = getSixDigitCode(code); + + if (isStockInWatchlist(code)) { + message.info(`${name} 已在自选中`); + return; + } + + try { + await dispatch( + toggleWatchlist({ + stockCode, + stockName: name, + isInWatchlist: false, + }) + ).unwrap(); + message.success(`已将 ${name}(${stockCode}) 添加到自选`); + } catch (error) { + console.error("添加自选失败:", error); + message.error("添加失败,请重试"); + } + }, + [dispatch, isStockInWatchlist] + ); + + // 加载股票行情 + const loadStockQuotes = useCallback( + async (stocks) => { + if (!stocks || stocks.length === 0) return; + setStockQuotesLoading(true); + const quotes = {}; + + for (const stock of stocks) { + const code = getSixDigitCode(stock.code); + try { + const response = await fetch( + `${getApiBase()}/api/market/trade/${code}?days=1` + ); + if (response.ok) { + const data = await response.json(); + if (data.success && data.data && data.data.length > 0) { + const latest = data.data[data.data.length - 1]; + quotes[stock.code] = { + price: latest.close, + change: latest.change_amount, + changePercent: latest.change_percent, + }; + } + } + } catch (err) { + console.error("加载股票行情失败:", code, err); + } + } + + setStockQuotes(quotes); + setStockQuotesLoading(false); + }, + [setStockQuotes, setStockQuotesLoading] + ); + + // 显示相关股票 + const showRelatedStocks = useCallback( + (stocks, eventTime, eventTitle) => { + if (!stocks || stocks.length === 0) return; + + // 归一化股票数据格式 + const normalizedStocks = stocks + .map((stock) => { + if (typeof stock === "object" && !Array.isArray(stock)) { + return { + code: stock.code || stock.stock_code || "", + name: stock.name || stock.stock_name || "", + description: stock.description || stock.relation_desc || "", + score: stock.score || 0, + report: stock.report || null, + }; + } + if (Array.isArray(stock)) { + return { + code: stock[0] || "", + name: stock[1] || "", + description: stock[2] || "", + score: stock[3] || 0, + report: null, + }; + } + return null; + }) + .filter(Boolean); + + // 按相关度排序 + const sortedStocks = normalizedStocks.sort( + (a, b) => (b.score || 0) - (a.score || 0) + ); + + setSelectedEventStocks(sortedStocks); + setSelectedEventTime(eventTime); + setSelectedEventTitle(eventTitle); + setStocksDrawerVisible(true); + setExpandedReasons({}); + loadStockQuotes(sortedStocks); + }, + [ + setSelectedEventStocks, + setSelectedEventTime, + setSelectedEventTitle, + setStocksDrawerVisible, + setExpandedReasons, + loadStockQuotes, + ] + ); + + // 相关股票表格列定义(和投资日历保持一致) + const stockColumns = useMemo( + () => + createStockColumns({ + stockQuotes, + expandedReasons, + setExpandedReasons, + showKline, + isStockInWatchlist, + addSingleToWatchlist, + }), + [ + stockQuotes, + expandedReasons, + setExpandedReasons, + showKline, + isStockInWatchlist, + addSingleToWatchlist, + ] + ); + + // 涨停板块表格列 - 精致风格设计 + const sectorColumns = useMemo( + () => + createSectorColumns({ + stockList, + setSelectedSectorInfo, + setSectorStocksModalVisible, + setSelectedRelatedEvents, + setRelatedEventsModalVisible, + }), + [ + stockList, + setSelectedSectorInfo, + setSectorStocksModalVisible, + setSelectedRelatedEvents, + setRelatedEventsModalVisible, + ] + ); + + // 涨停股票详情表格列 + const ztStockColumns = useMemo( + () => + createZtStockColumns({ + showContentDetail, + setSelectedKlineStock, + setKlineModalVisible, + isStockInWatchlist, + addSingleToWatchlist, + }), + [ + showContentDetail, + setSelectedKlineStock, + setKlineModalVisible, + isStockInWatchlist, + addSingleToWatchlist, + ] + ); + + // 事件表格列 + const eventColumns = useMemo( + () => createEventColumns({ showContentDetail, showRelatedStocks }), + [showContentDetail, showRelatedStocks] + ); + + // 条件返回必须在所有hooks之后 + if (!selectedDate) return null; + + const dateStr = `${selectedDate.getFullYear()}年${ + selectedDate.getMonth() + 1 + }月${selectedDate.getDate()}日`; + const isPastDate = selectedDate < new Date(new Date().setHours(0, 0, 0, 0)); + + return ( + <> + + +
+ +
+
+
+ {dateStr} +
+
+ + {isPastDate ? "历史数据" : "未来事件"} + + {ztDetail && ( + + 涨停 {ztDetail.total_stocks || 0} 家 + + )} + {events?.length > 0 && ( + + 事件 {events.length} 个 + + )} +
+
+ + } + styles={{ + header: { + background: "rgba(25,25,50,0.98)", + borderBottom: "1px solid rgba(255,215,0,0.2)", + padding: "16px 24px", + }, + body: { + background: + "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + padding: "24px", + maxHeight: "80vh", + overflowY: "auto", + }, + content: { + background: + "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + borderRadius: "16px", + border: "1px solid rgba(255,215,0,0.2)", + }, + mask: { + background: "rgba(0,0,0,0.7)", + backdropFilter: GLASS_BLUR.sm, + }, + }} + className="hero-panel-modal" + > + {loading ? ( +
+ +
+ ) : ( + + {/* 涨停分析 Tab */} + + + 涨停分析 ({ztDetail?.total_stocks || 0}) + + } + key="zt" + disabled={!ztDetail} + > + {sectorList.length > 0 || stockList.length > 0 ? ( +
+ {/* 热门关键词 */} + + + {/* 涨停统计卡片 */} + + + {/* 视图切换按钮 - 更精致的样式 */} +
+
+ + +
+
+
+ + + {ztDetail?.total_stocks || 0} + + + 只涨停 + +
+
+
+ + {/* 板块视图 */} + {ztViewMode === "sector" && ( + + )} + + {/* 个股视图 */} + {ztViewMode === "stock" && ( +
+ {/* 板块筛选器 */} +
+
+ + 板块筛选: + + +
+
+ {sectorList.slice(0, 10).map((sector) => ( + + ))} +
+
+ + {/* 筛选结果提示 */} + {selectedSectorFilter && ( +
+ + + 当前筛选:{selectedSectorFilter} + + + 共 {filteredStockList.length} 只 + + +
+ )} + +
+ + + + )} + + ) : ( +
+
+
+ +
+
+ + 暂无涨停数据 + + + 该日期没有涨停股票记录 + +
+
+
+ )} + + + {/* 未来事件 Tab */} + + + 未来事件 ({events?.length || 0}) + + } + key="event" + disabled={!events?.length} + > + + + + )} + + + + {/* 内容详情弹窗 - 页面居中 */} + + setDetailDrawerVisible(false)} + footer={null} + width={700} + centered + zIndex={1500} + styles={{ + header: { + background: "rgba(25,25,50,0.98)", + borderBottom: "1px solid rgba(255,215,0,0.2)", + padding: "16px 24px", + marginBottom: 0, + }, + body: { + background: + "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + padding: "24px", + maxHeight: "70vh", + overflowY: "auto", + }, + content: { + background: + "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + borderRadius: "12px", + border: "1px solid rgba(255,215,0,0.2)", + }, + mask: { + background: "rgba(0,0,0,0.6)", + }, + }} + > +
+
+ + {typeof selectedContent?.content === "string" + ? selectedContent.content + : selectedContent?.content?.data + ? selectedContent.content.data + .map((item) => item.sentence || "") + .join("\n\n") + : "暂无内容"} + +

+ (AI合成内容) +

+
+
+
+
+ + {/* 相关股票弹窗 */} + + { + setStocksDrawerVisible(false); + setExpandedReasons({}); + }} + footer={null} + width={1100} + centered + title={ +
+ +
+ + 相关股票 + + {selectedEventTitle && ( + + {selectedEventTitle} + + )} +
+ + {selectedEventStocks?.length || 0}只 + + {stockQuotesLoading && } +
+ } + styles={{ + header: { + background: "rgba(25,25,50,0.98)", + borderBottom: "1px solid rgba(255,215,0,0.2)", + padding: "16px 24px", + }, + body: { + background: + "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + padding: "16px", + maxHeight: "70vh", + overflowY: "auto", + }, + 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.2)", + borderRadius: "16px", + }, + mask: { + background: "rgba(0,0,0,0.7)", + backdropFilter: GLASS_BLUR.sm, + }, + }} + > + {selectedEventStocks && selectedEventStocks.length > 0 ? ( +
+
record.code} + size="middle" + pagination={false} + scroll={{ y: 500 }} + /> + + ) : ( +
+ + 暂无相关股票 + +
+ )} + + + + {/* K线图弹窗 */} + {selectedKlineStock && ( + { + setKlineModalVisible(false); + setSelectedKlineStock(null); + }} + stock={selectedKlineStock} + eventTime={selectedEventTime} + size="5xl" + /> + )} + + {/* 板块股票弹窗 */} + { + setSectorStocksModalVisible(false); + setSelectedSectorInfo(null); + }} + sectorInfo={selectedSectorInfo} + onShowKline={(record) => { + const code = record.scode; + let stockCode = code; + if (!code.includes(".")) { + if (code.startsWith("6")) stockCode = `${code}.SH`; + else if (code.startsWith("0") || code.startsWith("3")) + stockCode = `${code}.SZ`; + } + setSelectedKlineStock({ + stock_code: stockCode, + stock_name: record.sname, + }); + setKlineModalVisible(true); + }} + onAddToWatchlist={addSingleToWatchlist} + isStockInWatchlist={isStockInWatchlist} + /> + + {/* 关联事件弹窗 - 涨停归因详情 */} + { + setRelatedEventsModalVisible(false); + setSelectedRelatedEvents({ sectorName: "", events: [] }); + }} + sectorName={selectedRelatedEvents.sectorName} + events={selectedRelatedEvents.events} + count={selectedRelatedEvents.count} + /> + + ); +}; + +export default DetailModal; diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/EventsTabView.js b/src/views/Community/components/HeroPanel/components/DetailModal/EventsTabView.js new file mode 100644 index 00000000..cf0a5dc4 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/DetailModal/EventsTabView.js @@ -0,0 +1,51 @@ +// 未来事件视图组件 +// 展示选定日期的未来事件列表 + +import React, { memo } from "react"; +import { Table, Typography } from "antd"; +import { CalendarOutlined } from "@ant-design/icons"; + +const { Text: AntText } = Typography; + +/** + * 未来事件视图 + * @param {Array} events - 事件列表 + * @param {Array} columns - 表格列配置 + */ +const EventsTabView = memo(({ events, columns }) => { + // 无数据时的空状态 + if (!events?.length) { + return ( +
+
+ + + 暂无事件数据 + +
+
+ ); + } + + return ( +
+ ); +}); + +EventsTabView.displayName = "EventsTabView"; + +export default EventsTabView; diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/RelatedEventsModal.js b/src/views/Community/components/HeroPanel/components/DetailModal/RelatedEventsModal.js new file mode 100644 index 00000000..553e8e32 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/DetailModal/RelatedEventsModal.js @@ -0,0 +1,220 @@ +// HeroPanel - 关联事件弹窗(涨停归因详情) +// 使用 Ant Design Modal 保持与现有代码风格一致 +import React from "react"; +import { Modal as AntModal, Tag, ConfigProvider, theme } from "antd"; +import { FileText } from "lucide-react"; +import { GLASS_BLUR } from "@/constants/glassConfig"; + +/** + * 获取相关度颜色 + */ +const getRelevanceColor = (score) => { + if (score >= 80) return "#10B981"; + if (score >= 60) return "#F59E0B"; + return "#6B7280"; +}; + +/** + * 关联事件弹窗 - 涨停归因详情 + */ +const RelatedEventsModal = ({ + visible, + onClose, + sectorName = "", + events = [], + count = 0, +}) => { + return ( + + +
+ +
+
+
+ {sectorName} - 涨停归因 +
+
+ + 涨停 {count} 只 + + + 关联事件 {events?.length || 0} 条 + +
+
+ + } + styles={{ + header: { + background: "rgba(25,25,50,0.98)", + borderBottom: "1px solid rgba(96,165,250,0.2)", + padding: "16px 24px", + }, + body: { + background: "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + padding: "16px 24px", + maxHeight: "65vh", + overflowY: "auto", + }, + content: { + background: "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + borderRadius: "16px", + border: "1px solid rgba(96,165,250,0.3)", + }, + mask: { background: "rgba(0,0,0,0.7)", backdropFilter: GLASS_BLUR.sm }, + }} + > + {events?.length > 0 ? ( +
+ {events.map((event, idx) => { + const relevanceColor = getRelevanceColor(event.relevance_score || 0); + + return ( +
{ + window.open(`/community?event_id=${event.event_id}`, "_blank"); + }} + onMouseEnter={(e) => { + e.currentTarget.style.background = "rgba(40,40,70,0.9)"; + e.currentTarget.style.borderColor = "rgba(96,165,250,0.3)"; + e.currentTarget.style.transform = "translateY(-2px)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = "rgba(30,30,50,0.8)"; + e.currentTarget.style.borderColor = "rgba(255,255,255,0.06)"; + e.currentTarget.style.transform = "translateY(0)"; + }} + > +
+ {/* 标题 */} +
+
+ + + {event.title} + +
+ + 相关度 {event.relevance_score || 0} + +
+ + {/* 相关原因 */} + {event.relevance_reason && ( + + {event.relevance_reason} + + )} + + {/* 匹配概念 */} + {event.matched_concepts?.length > 0 && ( +
+ + 匹配概念: + +
+ {event.matched_concepts.slice(0, 6).map((concept, i) => ( + + {concept} + + ))} + {event.matched_concepts.length > 6 && ( + + +{event.matched_concepts.length - 6} + + )} +
+
+ )} +
+
+ ); + })} +
+ ) : ( +
+ 暂无关联事件 +
+ )} +
+
+ ); +}; + +export default RelatedEventsModal; diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/SectorStocksModal.js b/src/views/Community/components/HeroPanel/components/DetailModal/SectorStocksModal.js new file mode 100644 index 00000000..7a6123d7 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/DetailModal/SectorStocksModal.js @@ -0,0 +1,373 @@ +// HeroPanel - 板块股票弹窗 +// 使用 Ant Design Modal 保持与现有代码风格一致 +import React from "react"; +import { Modal as AntModal, Table, Tag, Button, Typography, ConfigProvider, theme } from "antd"; +import { TagsOutlined, LineChartOutlined, StarFilled, StarOutlined } from "@ant-design/icons"; +import { GLASS_BLUR } from "@/constants/glassConfig"; + +const { Text: AntText } = Typography; + +/** + * 获取连板天数样式 + */ +const getDaysStyle = (days) => { + if (days >= 5) + return { + bg: "linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)", + text: "#fff", + }; + if (days >= 3) + return { + bg: "linear-gradient(135deg, #fa541c 0%, #ff7a45 100%)", + text: "#fff", + }; + if (days >= 2) + return { + bg: "linear-gradient(135deg, #fa8c16 0%, #ffc53d 100%)", + text: "#fff", + }; + return { bg: "rgba(255,255,255,0.1)", text: "#888" }; +}; + +/** + * 获取涨停时间样式 + */ +const getTimeStyle = (time) => { + if (time <= "09:30:00") return { bg: "#ff4d4f", text: "#fff" }; + if (time <= "09:35:00") return { bg: "#fa541c", text: "#fff" }; + if (time <= "10:00:00") return { bg: "#fa8c16", text: "#fff" }; + return { bg: "rgba(255,255,255,0.1)", text: "#888" }; +}; + +/** + * 板块股票弹窗 + */ +const SectorStocksModal = ({ + visible, + onClose, + sectorInfo, + onShowKline, + onAddToWatchlist, + isStockInWatchlist, +}) => { + if (!sectorInfo) return null; + + const { name, count, stocks = [] } = sectorInfo; + + // 连板统计 + const stats = { 首板: 0, "2连板": 0, "3连板": 0, "4连板+": 0 }; + stocks.forEach((s) => { + const days = s._continuousDays || 1; + if (days === 1) stats["首板"]++; + else if (days === 2) stats["2连板"]++; + else if (days === 3) stats["3连板"]++; + else stats["4连板+"]++; + }); + + // 表格列定义 + const columns = [ + { + title: "股票", + key: "stock", + width: 130, + render: (_, record) => ( +
+ + {record.sname} + + + {record.scode} + +
+ ), + }, + { + title: "连板", + dataIndex: "continuous_days", + key: "continuous", + width: 90, + align: "center", + render: (text, record) => { + const days = record._continuousDays || 1; + const style = getDaysStyle(days); + return ( + + {text || "首板"} + + ); + }, + }, + { + title: "涨停时间", + dataIndex: "formatted_time", + key: "time", + width: 90, + align: "center", + render: (time) => { + const style = getTimeStyle(time || "15:00:00"); + return ( + + {time?.substring(0, 5) || "-"} + + ); + }, + }, + { + title: "核心板块", + dataIndex: "core_sectors", + key: "sectors", + render: (sectors) => ( +
+ {(sectors || []).slice(0, 2).map((sector, idx) => ( + + {sector} + + ))} +
+ ), + }, + { + title: "K线图", + key: "kline", + width: 80, + align: "center", + render: (_, record) => ( + + ), + }, + { + title: "操作", + key: "action", + width: 90, + align: "center", + render: (_, record) => { + const code = record.scode; + const inWatchlist = isStockInWatchlist(code); + return ( + + ); + }, + }, + ]; + + return ( + + +
+ +
+
+
+ + {name} + + + {count} 只涨停 + +
+
+ 按连板天数降序排列 +
+
+ + } + styles={{ + header: { + background: "rgba(25,25,50,0.98)", + borderBottom: "1px solid rgba(255,215,0,0.2)", + padding: "16px 24px", + }, + body: { + background: "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + padding: "16px 24px", + maxHeight: "70vh", + overflowY: "auto", + }, + content: { + background: "linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)", + borderRadius: "16px", + border: "1px solid rgba(255,215,0,0.2)", + }, + mask: { background: "rgba(0,0,0,0.7)", backdropFilter: GLASS_BLUR.sm }, + }} + > + {stocks.length > 0 ? ( +
+ {/* 快速统计 */} +
+ {Object.entries(stats).map( + ([key, value]) => + value > 0 && ( + + {key}: {value} + + ) + )} +
+ + {/* 股票列表 */} +
+
+ + + ) : ( +
+ 暂无股票数据 +
+ )} + + + ); +}; + +export default SectorStocksModal; diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/ZTSectorView.js b/src/views/Community/components/HeroPanel/components/DetailModal/ZTSectorView.js new file mode 100644 index 00000000..aeef4848 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/DetailModal/ZTSectorView.js @@ -0,0 +1,39 @@ +// 涨停板块视图组件 +// 展示按板块分组的涨停数据表格 + +import React, { memo } from "react"; +import { Table } from "antd"; + +/** + * 涨停板块视图 + * @param {Array} sectorList - 板块列表数据 + * @param {Array} columns - 表格列配置 + */ +const ZTSectorView = memo(({ sectorList, columns }) => { + if (!sectorList?.length) { + return null; + } + + return ( +
+
+ + ); +}); + +ZTSectorView.displayName = "ZTSectorView"; + +export default ZTSectorView; diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/ZTStockListView.js b/src/views/Community/components/HeroPanel/components/DetailModal/ZTStockListView.js new file mode 100644 index 00000000..ad909105 --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/DetailModal/ZTStockListView.js @@ -0,0 +1,134 @@ +// 涨停个股视图组件 +// 展示涨停个股列表,支持按板块筛选 + +import React, { memo } from "react"; +import { Table } from "antd"; + +/** + * 涨停个股视图 + * @param {Array} stockList - 完整股票列表 + * @param {Array} filteredStockList - 筛选后的股票列表 + * @param {Array} sectorList - 板块列表(用于筛选器) + * @param {Array} columns - 表格列配置 + * @param {string|null} selectedSectorFilter - 当前选中的板块筛选 + * @param {Function} onSectorFilterChange - 筛选变化回调 + */ +const ZTStockListView = memo(({ + stockList, + filteredStockList, + sectorList, + columns, + selectedSectorFilter, + onSectorFilterChange, +}) => { + if (!stockList?.length) { + return null; + } + + return ( +
+ {/* 板块筛选器 */} +
+
+ + 板块筛选: + + +
+
+ {sectorList.slice(0, 10).map((sector) => ( + + ))} +
+
+ + {/* 筛选结果提示 */} + {selectedSectorFilter && ( +
+ + {selectedSectorFilter} + + + 共 {filteredStockList.length} 只涨停 + + +
+ )} + + {/* 股票表格 */} +
+
+ + + ); +}); + +ZTStockListView.displayName = "ZTStockListView"; + +export default ZTStockListView; diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/index.js b/src/views/Community/components/HeroPanel/components/DetailModal/index.js new file mode 100644 index 00000000..307a084f --- /dev/null +++ b/src/views/Community/components/HeroPanel/components/DetailModal/index.js @@ -0,0 +1,7 @@ +// HeroPanel - DetailModal 子组件导出 +export { default as DetailModal } from "./DetailModal"; +export { default as RelatedEventsModal } from "./RelatedEventsModal"; +export { default as SectorStocksModal } from "./SectorStocksModal"; +export { default as ZTSectorView } from "./ZTSectorView"; +export { default as ZTStockListView } from "./ZTStockListView"; +export { default as EventsTabView } from "./EventsTabView";