From 654c6cff048dd4a33de0d9055f4a7ff01ef43c98 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Sat, 10 Jan 2026 18:41:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E4=B8=AD=E5=BF=83=E7=9A=84?= =?UTF-8?q?=E6=B6=A8=E5=81=9C=E5=8E=9F=E5=9B=A0=E9=87=8C=E9=9D=A2=E5=92=8C?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/ztStaticService.ts | 22 +- src/types/limitAnalyse.ts | 18 ++ .../RelatedEvents/RelatedEventCard.tsx | 239 ++++++++++++++++++ .../RelatedEvents/RelatedEventsModal.tsx | 150 +++++++++++ .../components/RelatedEvents/index.ts | 5 + .../components/SectorItem.tsx | 136 +++++++++- 6 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventCard.tsx create mode 100644 src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventsModal.tsx create mode 100644 src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/index.ts diff --git a/src/services/ztStaticService.ts b/src/services/ztStaticService.ts index 81c28c46..d7e74c9b 100644 --- a/src/services/ztStaticService.ts +++ b/src/services/ztStaticService.ts @@ -48,6 +48,22 @@ export interface StockRecord { [key: string]: unknown; } +/** 板块关联事件(涨停归因) */ +export interface RelatedEvent { + /** 事件ID */ + event_id: number; + /** 事件标题 */ + title: string; + /** 匹配的概念列表 */ + matched_concepts?: string[]; + /** 匹配分数(匹配的概念数量) */ + match_score?: number; + /** 相关度分数 0-100 */ + relevance_score?: number; + /** 相关原因说明 */ + relevance_reason?: string; +} + /** 板块信息 */ export interface SectorInfo { count: number; @@ -55,6 +71,8 @@ export interface SectorInfo { stock_codes?: string[]; net_inflow?: number; leading_stock?: string; + /** 关联事件列表(涨停归因) */ + related_events?: RelatedEvent[]; /** 允许额外字段 */ [key: string]: unknown; } @@ -354,7 +372,7 @@ export const fetchDailyAnalysis = async (date: string, forceRefresh = false): Pr stockMap[stock.scode] = stock; }); - // 转换 sector_data 中的 stock_codes 为 stocks + // 转换 sector_data 中的 stock_codes 为 stocks,同时保留 related_events const transformedSectorData: Record = {}; if (rawData.sector_data) { Object.entries(rawData.sector_data).forEach(([sectorName, sectorInfo]) => { @@ -364,6 +382,8 @@ export const fetchDailyAnalysis = async (date: string, forceRefresh = false): Pr transformedSectorData[sectorName] = { count: sectorInfo.count, stocks: stocks, + // 保留关联事件数据(新数据结构) + related_events: sectorInfo.related_events, }; }); } diff --git a/src/types/limitAnalyse.ts b/src/types/limitAnalyse.ts index 0c8120c1..74f6cf9d 100644 --- a/src/types/limitAnalyse.ts +++ b/src/types/limitAnalyse.ts @@ -60,6 +60,22 @@ export interface HighPositionStock { // ==================== 板块相关 ==================== +/** 板块关联事件(涨停归因) */ +export interface RelatedEvent { + /** 事件ID */ + event_id: number; + /** 事件标题 */ + title: string; + /** 匹配的概念列表 */ + matched_concepts?: string[]; + /** 匹配分数(匹配的概念数量) */ + match_score?: number; + /** 相关度分数 0-100 */ + relevance_score?: number; + /** 相关原因说明 */ + relevance_reason?: string; +} + /** 板块数据 */ export interface SectorData { /** 涨停股票数量 */ @@ -72,6 +88,8 @@ export interface SectorData { leading_stock?: string; /** 股票代码列表 */ stock_codes?: string[]; + /** 关联事件列表(涨停归因) */ + related_events?: RelatedEvent[]; /** 允许额外字段 */ [key: string]: unknown; } diff --git a/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventCard.tsx b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventCard.tsx new file mode 100644 index 00000000..884a3d1a --- /dev/null +++ b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventCard.tsx @@ -0,0 +1,239 @@ +/** + * RelatedEventCard - 关联事件卡片组件 + * 在板块卡片内显示涨停归因的关联事件 + */ +import React, { memo } from 'react'; +import { + Box, + HStack, + VStack, + Text, + Badge, + Icon, + Progress, + Tooltip, +} from '@chakra-ui/react'; +import { Newspaper, TrendingUp, ExternalLink } from 'lucide-react'; +import type { RelatedEvent } from '@/types/limitAnalyse'; +import { textColors, bgColors } from '@/constants/limitAnalyseTheme'; + +interface RelatedEventCardProps { + /** 事件数据 */ + event: RelatedEvent; + /** 是否为紧凑模式(板块卡片内摘要显示) */ + compact?: boolean; + /** 点击事件回调 */ + onClick?: (event: RelatedEvent) => void; +} + +/** + * 根据相关度分数获取颜色 + */ +const getRelevanceColor = (score: number): string => { + if (score >= 80) return '#10B981'; // 高相关 - 绿色 + if (score >= 60) return '#F59E0B'; // 中相关 - 橙色 + return '#6B7280'; // 低相关 - 灰色 +}; + +/** + * 根据相关度分数获取标签 + */ +const getRelevanceLabel = (score: number): string => { + if (score >= 80) return '高度相关'; + if (score >= 60) return '中度相关'; + return '参考'; +}; + +/** + * RelatedEventCard 组件 + */ +const RelatedEventCard: React.FC = memo(({ + event, + compact = false, + onClick +}) => { + const relevanceColor = getRelevanceColor(event.relevance_score || 0); + const relevanceLabel = getRelevanceLabel(event.relevance_score || 0); + + const handleClick = () => { + onClick?.(event); + }; + + // 紧凑模式(板块卡片内摘要) + if (compact) { + return ( + + + + + + {event.title} + + + + {relevanceLabel} + + {event.match_score && event.match_score > 1 && ( + + 匹配{event.match_score}个概念 + + )} + + + + + ); + } + + // 完整模式(弹窗列表) + return ( + + + {/* 标题和跳转图标 */} + + + + + {event.title} + + + {onClick && ( + + )} + + + {/* 相关度分数 */} + + + + + 相关度 + + + + div': { + background: `linear-gradient(90deg, ${relevanceColor}80, ${relevanceColor})`, + }, + }} + /> + + + {event.relevance_score || 0}分 + + + + {/* 匹配的概念 */} + {event.matched_concepts && event.matched_concepts.length > 0 && ( + + + 匹配概念: + + + {event.matched_concepts.slice(0, 5).map((concept, idx) => ( + + {concept} + + ))} + {event.matched_concepts.length > 5 && ( + + +{event.matched_concepts.length - 5} + + )} + + + )} + + {/* 相关原因 */} + {event.relevance_reason && ( + + + {event.relevance_reason} + + + )} + + + ); +}); + +RelatedEventCard.displayName = 'RelatedEventCard'; + +export default RelatedEventCard; diff --git a/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventsModal.tsx b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventsModal.tsx new file mode 100644 index 00000000..429ee2bd --- /dev/null +++ b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/RelatedEventsModal.tsx @@ -0,0 +1,150 @@ +/** + * RelatedEventsModal - 关联事件弹窗 + * 显示板块完整的关联事件列表 + */ +import React, { memo, useCallback } from 'react'; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + VStack, + HStack, + Text, + Badge, + Icon, + Box, + Divider, +} from '@chakra-ui/react'; +import { Newspaper, TrendingUp, Layers } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import type { RelatedEvent } from '@/types/limitAnalyse'; +import { textColors, bgColors, borderColors, goldColors } from '@/constants/limitAnalyseTheme'; +import { scrollbarStyles } from '@/styles/limitAnalyseStyles'; +import RelatedEventCard from './RelatedEventCard'; + +interface RelatedEventsModalProps { + /** 是否打开 */ + isOpen: boolean; + /** 关闭回调 */ + onClose: () => void; + /** 板块名称 */ + sectorName: string; + /** 关联事件列表 */ + events: RelatedEvent[]; + /** 板块涨停数量 */ + limitUpCount?: number; +} + +/** + * RelatedEventsModal 组件 + */ +const RelatedEventsModal: React.FC = memo(({ + isOpen, + onClose, + sectorName, + events, + limitUpCount = 0, +}) => { + const navigate = useNavigate(); + + // 点击事件跳转到事件详情 + const handleEventClick = useCallback((event: RelatedEvent) => { + onClose(); + navigate(`/community?event_id=${event.event_id}`); + }, [navigate, onClose]); + + // 按相关度排序 + const sortedEvents = [...events].sort( + (a, b) => (b.relevance_score || 0) - (a.relevance_score || 0) + ); + + // 高相关事件数量 + const highRelevanceCount = events.filter(e => (e.relevance_score || 0) >= 80).length; + + return ( + + + + {/* 头部 */} + + + + + + {sectorName} - 涨停归因分析 + + + + + + + 涨停 {limitUpCount} 只 + + + + + + 关联事件 {events.length} 条 + + + {highRelevanceCount > 0 && ( + + 高度相关 {highRelevanceCount} + + )} + + + + + + + + + {/* 事件列表 */} + + {sortedEvents.length === 0 ? ( + + + 暂无关联事件 + + ) : ( + + {sortedEvents.map((event, index) => ( + + ))} + + )} + + + + ); +}); + +RelatedEventsModal.displayName = 'RelatedEventsModal'; + +export default RelatedEventsModal; diff --git a/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/index.ts b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/index.ts new file mode 100644 index 00000000..3666fb48 --- /dev/null +++ b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/RelatedEvents/index.ts @@ -0,0 +1,5 @@ +/** + * RelatedEvents 组件导出 + */ +export { default as RelatedEventCard } from './RelatedEventCard'; +export { default as RelatedEventsModal } from './RelatedEventsModal'; diff --git a/src/views/LimitAnalyse/components/UnifiedSectorCard/components/SectorItem.tsx b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/SectorItem.tsx index 4756a882..31cbe528 100644 --- a/src/views/LimitAnalyse/components/UnifiedSectorCard/components/SectorItem.tsx +++ b/src/views/LimitAnalyse/components/UnifiedSectorCard/components/SectorItem.tsx @@ -2,7 +2,7 @@ * 单个板块项组件 * 支持 accordion 和 card 两种显示模式 */ -import React, { memo, useCallback, useMemo } from "react"; +import React, { memo, useCallback, useMemo, useState } from "react"; import { Box, VStack, @@ -22,8 +22,11 @@ import { AccordionButton, AccordionPanel, AccordionIcon, + Button, + useDisclosure, + Divider, } from "@chakra-ui/react"; -import { Star, Zap, ChevronDown, ChevronUp, Sparkles } from "lucide-react"; +import { Star, Zap, ChevronDown, ChevronUp, Sparkles, Newspaper, ChevronRight } from "lucide-react"; import type { SectorData, LimitUpStock } from "@/types/limitAnalyse"; import { formatLimitUpTime, getSectorColor } from "@/utils/limitAnalyseUtils"; import { @@ -34,6 +37,7 @@ import { SECTOR_COLORS, } from "@/constants/limitAnalyseTheme"; import StockListItem from "./StockListItem"; +import { RelatedEventCard, RelatedEventsModal } from "./RelatedEvents"; /** 获取板块渐变色 */ function getSectorGradient(sector: string): string { @@ -103,9 +107,13 @@ interface SectorItemProps { const SectorItem = memo( ({ sector, data, index, isExpanded, onToggle, displayMode = "card" }) => { const stocks = data.stocks || []; + const relatedEvents = data.related_events || []; const leadingStock = stocks[0]; const sectorColor = getSectorSingleColor(sector); + // 关联事件弹窗控制 + const { isOpen: isEventsModalOpen, onOpen: onOpenEventsModal, onClose: onCloseEventsModal } = useDisclosure(); + const firstLimitTime = leadingStock ? formatLimitUpTime(leadingStock) : "-"; @@ -117,6 +125,19 @@ const SectorItem = memo( ); }, [stocks]); + // 按相关度排序的事件(取前2条用于摘要显示) + const topEvents = useMemo(() => { + return [...relatedEvents] + .sort((a, b) => (b.relevance_score || 0) - (a.relevance_score || 0)) + .slice(0, 2); + }, [relatedEvents]); + + // 点击查看更多事件 + const handleViewMoreEvents = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + onOpenEventsModal(); + }, [onOpenEventsModal]); + // Card 模式 if (displayMode === "card") { return ( @@ -250,6 +271,52 @@ const SectorItem = memo( {/* Body - 展开的个股明细 */} + {/* 关联事件区域 */} + {relatedEvents.length > 0 && ( + + + + + + 涨停归因 + + + {relatedEvents.length}条 + + + {relatedEvents.length > 2 && ( + + )} + + + {topEvents.map((event) => ( + + ))} + + + )} + + {/* 股票列表 */} {sortedStocks.map((stock, idx) => ( ( + + {/* 关联事件弹窗 */} + ); } @@ -317,6 +393,53 @@ const SectorItem = memo( + {/* 关联事件区域 */} + {relatedEvents.length > 0 && ( + + + + + + 涨停归因 + + + {relatedEvents.length}条 + + + {relatedEvents.length > 2 && ( + + )} + + + {topEvents.map((event) => ( + + ))} + + + + )} + + {/* 股票列表 */} {sortedStocks.map((stock, idx) => ( ( ))} + + {/* 关联事件弹窗 */} + ); }