// src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js // 主线时间轴布局组件 - 按 lv2 概念分组展示事件(横向滚动布局) import React, { useState, useEffect, forwardRef, useImperativeHandle, useCallback, } from "react"; import { Box, VStack, HStack, Text, Badge, Flex, Icon, Collapse, Spinner, Center, IconButton, useColorModeValue, Tooltip, } from "@chakra-ui/react"; import { ChevronDownIcon, ChevronUpIcon, RepeatIcon, } from "@chakra-ui/icons"; import { FiTrendingUp, FiZap } from "react-icons/fi"; import DynamicNewsEventCard from "../../EventCard/DynamicNewsEventCard"; import { getApiBase } from "@utils/apiConfig"; /** * 主线时间轴布局组件 * * 功能: * 1. 调用 /api/events/mainline 获取预分组数据 * 2. 按 lv2 概念分组展示,横向滚动布局 * 3. 按事件数量从多到少排序 * 4. 深色背景风格,与列表模式统一 */ const MainlineTimelineViewComponent = forwardRef( ( { display = "block", filters = {}, selectedEvent, onEventSelect, eventFollowStatus = {}, onToggleFollow, borderColor, }, ref ) => { // 状态 const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [mainlineData, setMainlineData] = useState(null); const [expandedGroups, setExpandedGroups] = useState({}); // 深色主题颜色 const containerBg = useColorModeValue("gray.50", "#1a1d24"); const cardBg = useColorModeValue("white", "#252a34"); const cardBorderColor = useColorModeValue("gray.200", "#3a3f4b"); const headerHoverBg = useColorModeValue("gray.100", "#2d323e"); const textColor = useColorModeValue("gray.800", "#e2e8f0"); const secondaryTextColor = useColorModeValue("gray.600", "#a0aec0"); const timelineLineColor = useColorModeValue("blue.300", "#4299e1"); const timelineDotBg = useColorModeValue("blue.500", "#63b3ed"); const scrollbarTrackBg = useColorModeValue("#e2e8f0", "#1a1d24"); const scrollbarThumbBg = useColorModeValue("#a0aec0", "#4a5568"); const scrollbarThumbHoverBg = useColorModeValue("#718096", "#718096"); const statBarBg = useColorModeValue("gray.100", "#252a34"); // 根据主线类型获取配色 const getColorScheme = useCallback((lv2Name) => { if (!lv2Name) return "gray"; const name = lv2Name.toLowerCase(); if ( name.includes("ai") || name.includes("人工智能") || name.includes("算力") || name.includes("大模型") ) return "purple"; if ( name.includes("半导体") || name.includes("芯片") || name.includes("光刻") ) return "blue"; if (name.includes("机器人") || name.includes("人形")) return "pink"; if ( name.includes("消费电子") || name.includes("手机") || name.includes("xr") ) return "cyan"; if ( name.includes("汽车") || name.includes("驾驶") || name.includes("新能源车") ) return "teal"; if ( name.includes("新能源") || name.includes("电力") || name.includes("光伏") || name.includes("储能") ) return "green"; if ( name.includes("低空") || name.includes("航天") || name.includes("卫星") ) return "orange"; if (name.includes("军工") || name.includes("国防")) return "red"; if ( name.includes("医药") || name.includes("医疗") || name.includes("生物") ) return "messenger"; if ( name.includes("消费") || name.includes("食品") || name.includes("白酒") ) return "yellow"; if ( name.includes("煤炭") || name.includes("石油") || name.includes("钢铁") ) return "blackAlpha"; if ( name.includes("金融") || name.includes("银行") || name.includes("券商") ) return "linkedin"; return "gray"; }, []); // 加载主线数据 const fetchMainlineData = useCallback(async () => { if (display === "none") return; setLoading(true); setError(null); try { const apiBase = getApiBase(); const params = new URLSearchParams(); // 添加筛选参数 if (filters.recent_days) params.append("recent_days", filters.recent_days); if (filters.importance && filters.importance !== "all") params.append("importance", filters.importance); const url = `${apiBase}/api/events/mainline?${params.toString()}`; console.log("[MainlineTimelineView] 🔄 请求主线数据:", url); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log("[MainlineTimelineView] 📦 响应数据:", { success: result.success, mainlineCount: result.data?.mainlines?.length, totalEvents: result.data?.total_events, }); if (result.success) { // 按事件数量从多到少排序 const sortedMainlines = [...(result.data.mainlines || [])].sort( (a, b) => b.event_count - a.event_count ); setMainlineData({ ...result.data, mainlines: sortedMainlines, }); // 初始化展开状态(默认全部折叠) const initialExpanded = {}; sortedMainlines.forEach((mainline) => { initialExpanded[mainline.lv2_id] = false; }); setExpandedGroups(initialExpanded); } else { throw new Error(result.error || "获取数据失败"); } } catch (err) { console.error("[MainlineTimelineView] ❌ 请求失败:", err); setError(err.message); } finally { setLoading(false); } }, [display, filters.recent_days, filters.importance]); // 初始加载 & 筛选变化时刷新 useEffect(() => { fetchMainlineData(); }, [fetchMainlineData]); // 暴露方法给父组件 useImperativeHandle( ref, () => ({ refresh: fetchMainlineData, getScrollPosition: () => null, }), [fetchMainlineData] ); // 切换分组展开/折叠 const toggleGroup = useCallback((lv2Id) => { setExpandedGroups((prev) => ({ ...prev, [lv2Id]: !prev[lv2Id], })); }, []); // 全部展开/折叠 const toggleAll = useCallback( (expand) => { if (!mainlineData?.mainlines) return; const newState = {}; mainlineData.mainlines.forEach((mainline) => { newState[mainline.lv2_id] = expand; }); setExpandedGroups(newState); }, [mainlineData] ); // 渲染加载状态 if (loading) { return (
正在加载主线数据...
); } // 渲染错误状态 if (error) { return (
加载失败: {error} } colorScheme="blue" onClick={fetchMainlineData} aria-label="重试" />
); } // 渲染空状态 if (!mainlineData?.mainlines?.length) { return (
暂无主线数据 尝试调整筛选条件
); } const { mainlines, total_events, mainline_count, ungrouped_count, } = mainlineData; return ( {/* 顶部统计栏 */} {mainline_count} 条主线 共 {total_events} 个事件 {ungrouped_count > 0 && ( {ungrouped_count} 个未归类 )} } size="sm" variant="ghost" colorScheme="gray" onClick={() => toggleAll(true)} aria-label="全部展开" /> } size="sm" variant="ghost" colorScheme="gray" onClick={() => toggleAll(false)} aria-label="全部折叠" /> } size="sm" variant="ghost" colorScheme="gray" onClick={fetchMainlineData} aria-label="刷新" /> {/* 主线卡片横向滚动容器 */} {mainlines.map((mainline) => { const colorScheme = getColorScheme(mainline.lv2_name); const isExpanded = expandedGroups[mainline.lv2_id]; return ( {/* 卡片头部 */} toggleGroup(mainline.lv2_id)} _hover={{ bg: headerHoverBg }} transition="all 0.15s" borderBottomWidth="1px" borderBottomColor={cardBorderColor} flexShrink={0} > {mainline.lv2_name || "其他"} {mainline.event_count} {mainline.lv1_name && ( {mainline.lv1_name} )} {/* 事件列表 - 可滚动到底 */} {/* 时间轴线 */} {/* 事件列表 */} {mainline.events.map((event, eventIndex) => ( {/* 时间轴圆点 */} {/* 事件卡片 */} { onEventSelect?.(clickedEvent); }} onTitleClick={(e) => { e.preventDefault(); e.stopPropagation(); onEventSelect?.(event); }} onToggleFollow={() => onToggleFollow?.(event.id)} borderColor={borderColor} compact /> ))} {/* 折叠时显示简要信息 */} {!isExpanded && ( {mainline.events.slice(0, 3).map((event) => ( { e.stopPropagation(); onEventSelect?.(event); }} > • {event.title} ))} {mainline.events.length > 3 && ( ... 还有 {mainline.events.length - 3} 条 )} )} ); })} ); } ); MainlineTimelineViewComponent.displayName = "MainlineTimelineView"; const MainlineTimelineView = React.memo(MainlineTimelineViewComponent); export default MainlineTimelineView;