From 96c94eaec4ce6762a21c9d0e7aad2c4763e463e6 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Mon, 22 Dec 2025 10:41:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0Company=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84UI=E4=B8=BAFUI=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 36 +++- .../layouts/MainlineTimelineView.js | 162 +++++++----------- .../components/EventCard/MiniEventCard.js | 151 ++++++++++++++++ 3 files changed, 250 insertions(+), 99 deletions(-) create mode 100644 src/views/Community/components/EventCard/MiniEventCard.js diff --git a/app.py b/app.py index 1d32b55b..1c999aa8 100755 --- a/app.py +++ b/app.py @@ -11216,7 +11216,33 @@ def get_events_by_mainline(): else: ungrouped_events.append(event_data) - # ==================== 5. 整理返回数据 ==================== + # ==================== 5. 获取 lv2 概念涨跌幅 ==================== + lv2_price_map = {} + try: + # 获取所有 lv2 名称 + lv2_names = [group['lv2_name'] for group in mainline_groups.values() if group.get('lv2_name')] + if lv2_names: + # 查询 concept_daily_stats 表获取最新涨跌幅 + price_sql = text(''' + SELECT concept_name, avg_change_pct, trade_date + FROM concept_daily_stats + WHERE concept_type = 'lv2' + AND concept_name IN :names + AND trade_date = ( + SELECT MAX(trade_date) FROM concept_daily_stats WHERE concept_type = 'lv2' + ) + ''') + price_result = db.session.execute(price_sql, {'names': tuple(lv2_names)}).fetchall() + for row in price_result: + lv2_price_map[row.concept_name] = { + 'avg_change_pct': float(row.avg_change_pct) if row.avg_change_pct else None, + 'trade_date': str(row.trade_date) if row.trade_date else None + } + app.logger.info(f'[mainline] 获取 lv2 涨跌幅: {len(lv2_price_map)} 条') + except Exception as price_err: + app.logger.warning(f'[mainline] 获取 lv2 涨跌幅失败: {price_err}') + + # ==================== 6. 整理返回数据 ==================== mainlines = [] for lv2_id, group in mainline_groups.items(): # 按时间倒序排列(不限制数量) @@ -11226,6 +11252,14 @@ def get_events_by_mainline(): reverse=True ) group['event_count'] = len(group['events']) + # 添加涨跌幅数据 + lv2_name = group.get('lv2_name', '') + if lv2_name in lv2_price_map: + group['avg_change_pct'] = lv2_price_map[lv2_name]['avg_change_pct'] + group['price_date'] = lv2_price_map[lv2_name]['trade_date'] + else: + group['avg_change_pct'] = None + group['price_date'] = None mainlines.append(group) # 按事件数量排序 diff --git a/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js b/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js index 6d1c64e3..4c826aa5 100644 --- a/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js +++ b/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js @@ -22,6 +22,7 @@ import { IconButton, Tooltip, Button, + SimpleGrid, } from "@chakra-ui/react"; import { ChevronDownIcon, @@ -29,7 +30,7 @@ import { RepeatIcon, } from "@chakra-ui/icons"; import { FiTrendingUp, FiZap } from "react-icons/fi"; -import DynamicNewsEventCard from "../../EventCard/DynamicNewsEventCard"; +import MiniEventCard from "../../EventCard/MiniEventCard"; import { getApiBase } from "@utils/apiConfig"; // 固定深色主题颜色 @@ -40,8 +41,6 @@ const COLORS = { headerHoverBg: "#2d323e", textColor: "#e2e8f0", secondaryTextColor: "#a0aec0", - timelineLineColor: "#4299e1", - timelineDotBg: "#63b3ed", scrollbarTrackBg: "#1a1d24", scrollbarThumbBg: "#4a5568", scrollbarThumbHoverBg: "#718096", @@ -49,7 +48,7 @@ const COLORS = { }; // 每次加载的事件数量 -const EVENTS_PER_LOAD = 10; +const EVENTS_PER_LOAD = 12; /** * 单个主线卡片组件 - 支持懒加载 @@ -61,9 +60,6 @@ const MainlineCard = React.memo(({ onToggle, selectedEvent, onEventSelect, - eventFollowStatus, - onToggleFollow, - borderColor, }) => { // 懒加载状态 const [displayCount, setDisplayCount] = useState(EVENTS_PER_LOAD); @@ -85,9 +81,9 @@ const MainlineCard = React.memo(({ const hasMore = displayCount < mainline.events.length; // 加载更多 - const loadMore = useCallback(() => { + const loadMore = useCallback((e) => { + e.stopPropagation(); setIsLoadingMore(true); - // 使用 setTimeout 模拟异步,避免 UI 卡顿 setTimeout(() => { setDisplayCount(prev => Math.min(prev + EVENTS_PER_LOAD, mainline.events.length)); setIsLoadingMore(false); @@ -102,8 +98,8 @@ const MainlineCard = React.memo(({ borderColor={COLORS.cardBorderColor} borderTopWidth="3px" borderTopColor={`${colorScheme}.500`} - minW={isExpanded ? "320px" : "200px"} - maxW={isExpanded ? "380px" : "240px"} + minW={isExpanded ? "400px" : "200px"} + maxW={isExpanded ? "500px" : "240px"} h="100%" display="flex" flexDirection="column" @@ -149,11 +145,24 @@ const MainlineCard = React.memo(({ {mainline.event_count} - {mainline.lv1_name && ( - - {mainline.lv1_name} - - )} + + {mainline.lv1_name && ( + + {mainline.lv1_name} + + )} + {/* 涨跌幅显示 */} + {mainline.avg_change_pct != null && ( + = 0 ? "#fc8181" : "#68d391"} + > + {mainline.avg_change_pct >= 0 ? "+" : ""} + {mainline.avg_change_pct.toFixed(2)}% + + )} + - {/* 时间轴线 */} - - - {/* 事件列表 */} - - {displayedEvents.map((event, eventIndex) => ( - - {/* 时间轴圆点 */} - - {/* 事件卡片 */} - { - onEventSelect?.(clickedEvent); - }} - onTitleClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - onEventSelect?.(event); - }} - onToggleFollow={() => onToggleFollow?.(event.id)} - borderColor={borderColor} - compact - /> - + {/* 事件网格 - 2列布局 */} + + {displayedEvents.map((event) => ( + ))} + - {/* 加载更多按钮 */} - {hasMore && ( - - )} - + {/* 加载更多按钮 */} + {hasMore && ( + + )} ) : ( /* 折叠时显示简要信息 */ - {mainline.events.slice(0, 3).map((event) => ( + {mainline.events.slice(0, 4).map((event) => ( ))} - {mainline.events.length > 3 && ( + {mainline.events.length > 4 && ( - ... 还有 {mainline.events.length - 3} 条 + ... 还有 {mainline.events.length - 4} 条 )} @@ -428,10 +391,10 @@ const MainlineTimelineViewComponent = forwardRef( mainlines: sortedMainlines, }); - // 初始化展开状态(默认全部折叠) + // 初始化展开状态(默认全部展开) const initialExpanded = {}; sortedMainlines.forEach((mainline) => { - initialExpanded[mainline.lv2_id] = false; + initialExpanded[mainline.lv2_id] = true; }); setExpandedGroups(initialExpanded); } else { @@ -610,7 +573,7 @@ const MainlineTimelineViewComponent = forwardRef( - {/* 主线卡片横向滚动容器 */} + {/* 主线卡片横向滚动容器 - 滚动条在顶部 */} {mainlines.map((mainline) => ( toggleGroup(mainline.lv2_id)} selectedEvent={selectedEvent} onEventSelect={onEventSelect} - eventFollowStatus={eventFollowStatus} - onToggleFollow={onToggleFollow} - borderColor={borderColor} /> ))} diff --git a/src/views/Community/components/EventCard/MiniEventCard.js b/src/views/Community/components/EventCard/MiniEventCard.js new file mode 100644 index 00000000..fcf2f007 --- /dev/null +++ b/src/views/Community/components/EventCard/MiniEventCard.js @@ -0,0 +1,151 @@ +// src/views/Community/components/EventCard/MiniEventCard.js +// 迷你事件卡片组件 - 用于主线模式的紧凑显示 + +import React from 'react'; +import { + Box, + Text, + HStack, + Tooltip, + Badge, +} from '@chakra-ui/react'; +import dayjs from 'dayjs'; +import { getImportanceConfig } from '@constants/importanceLevels'; + +// 固定深色主题颜色 +const COLORS = { + cardBg: "#2d323e", + cardHoverBg: "#363c4a", + cardBorderColor: "#4a5568", + textColor: "#e2e8f0", + secondaryTextColor: "#a0aec0", + linkColor: "#63b3ed", + selectedBg: "#2c5282", + selectedBorderColor: "#4299e1", +}; + +/** + * 迷你事件卡片组件 + * 紧凑的卡片式布局,适合在主线模式中横向排列 + */ +const MiniEventCard = React.memo(({ + event, + isSelected = false, + onEventClick, +}) => { + const importance = getImportanceConfig(event.importance); + + // 格式化时间为简短形式 + const formatTime = (timestamp) => { + const date = dayjs(timestamp); + const now = dayjs(); + const diffDays = now.diff(date, 'day'); + + if (diffDays === 0) { + return date.format('HH:mm'); + } else if (diffDays === 1) { + return '昨天 ' + date.format('HH:mm'); + } else if (diffDays < 7) { + return date.format('MM-DD HH:mm'); + } else { + return date.format('MM-DD'); + } + }; + + // 获取涨跌幅显示 + const getChangeDisplay = () => { + const avgChange = event.related_avg_chg; + if (avgChange == null || isNaN(Number(avgChange))) { + return null; + } + const numChange = Number(avgChange); + const isPositive = numChange > 0; + return { + value: `${isPositive ? '+' : ''}${numChange.toFixed(1)}%`, + color: isPositive ? '#fc8181' : '#68d391', + }; + }; + + const changeDisplay = getChangeDisplay(); + + return ( + + {event.title} + + {dayjs(event.created_at).format('YYYY-MM-DD HH:mm')} + + {event.description && ( + + {event.description} + + )} + + } + placement="top" + hasArrow + bg="gray.800" + color="white" + p={3} + borderRadius="md" + maxW="300px" + > + onEventClick?.(event)} + _hover={{ + bg: isSelected ? COLORS.selectedBg : COLORS.cardHoverBg, + borderColor: importance.color || COLORS.cardBorderColor, + transform: 'translateY(-1px)', + }} + transition="all 0.15s ease" + minW="0" + > + {/* 第一行:时间 + 涨跌幅 */} + + + {formatTime(event.created_at)} + + {changeDisplay && ( + + {changeDisplay.value} + + )} + + + {/* 第二行:标题 */} + + {event.title} + + + + ); +}); + +MiniEventCard.displayName = 'MiniEventCard'; + +export default MiniEventCard;