// src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js // 主线时间轴布局组件 - 按 lv2 概念分组展示事件(横向滚动布局) import React, { useState, useEffect, forwardRef, useImperativeHandle, useCallback, useMemo, } from "react"; import { Box, VStack, HStack, Text, Badge, Flex, Icon, Spinner, Center, IconButton, Tooltip, Button, } 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"; // 固定深色主题颜色 const COLORS = { containerBg: "#1a1d24", cardBg: "#252a34", cardBorderColor: "#3a3f4b", headerHoverBg: "#2d323e", textColor: "#e2e8f0", secondaryTextColor: "#a0aec0", timelineLineColor: "#4299e1", timelineDotBg: "#63b3ed", scrollbarTrackBg: "#1a1d24", scrollbarThumbBg: "#4a5568", scrollbarThumbHoverBg: "#718096", statBarBg: "#252a34", }; // 每次加载的事件数量 const EVENTS_PER_LOAD = 10; /** * 单个主线卡片组件 - 支持懒加载 */ const MainlineCard = React.memo(({ mainline, colorScheme, isExpanded, onToggle, selectedEvent, onEventSelect, eventFollowStatus, onToggleFollow, borderColor, }) => { // 懒加载状态 const [displayCount, setDisplayCount] = useState(EVENTS_PER_LOAD); const [isLoadingMore, setIsLoadingMore] = useState(false); // 重置显示数量当折叠时 useEffect(() => { if (!isExpanded) { setDisplayCount(EVENTS_PER_LOAD); } }, [isExpanded]); // 当前显示的事件 const displayedEvents = useMemo(() => { return mainline.events.slice(0, displayCount); }, [mainline.events, displayCount]); // 是否还有更多 const hasMore = displayCount < mainline.events.length; // 加载更多 const loadMore = useCallback(() => { setIsLoadingMore(true); // 使用 setTimeout 模拟异步,避免 UI 卡顿 setTimeout(() => { setDisplayCount(prev => Math.min(prev + EVENTS_PER_LOAD, mainline.events.length)); setIsLoadingMore(false); }, 50); }, [mainline.events.length]); return ( {/* 卡片头部 */} {mainline.lv2_name || "其他"} {mainline.event_count} {mainline.lv1_name && ( {mainline.lv1_name} )} {/* 事件列表区域 */} {isExpanded ? ( {/* 时间轴线 */} {/* 事件列表 */} {displayedEvents.map((event, eventIndex) => ( {/* 时间轴圆点 */} {/* 事件卡片 */} { onEventSelect?.(clickedEvent); }} onTitleClick={(e) => { e.preventDefault(); e.stopPropagation(); onEventSelect?.(event); }} onToggleFollow={() => onToggleFollow?.(event.id)} borderColor={borderColor} compact /> ))} {/* 加载更多按钮 */} {hasMore && ( )} ) : ( /* 折叠时显示简要信息 */ {mainline.events.slice(0, 3).map((event) => ( { e.stopPropagation(); onEventSelect?.(event); }} > • {event.title} ))} {mainline.events.length > 3 && ( ... 还有 {mainline.events.length - 3} 条 )} )} ); }); MainlineCard.displayName = "MainlineCard"; /** * 主线时间轴布局组件 */ 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 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" color={COLORS.secondaryTextColor} onClick={() => toggleAll(true)} aria-label="全部展开" _hover={{ bg: COLORS.headerHoverBg }} /> } size="sm" variant="ghost" color={COLORS.secondaryTextColor} onClick={() => toggleAll(false)} aria-label="全部折叠" _hover={{ bg: COLORS.headerHoverBg }} /> } size="sm" variant="ghost" color={COLORS.secondaryTextColor} onClick={fetchMainlineData} aria-label="刷新" _hover={{ bg: COLORS.headerHoverBg }} /> {/* 主线卡片横向滚动容器 */} {mainlines.map((mainline) => ( toggleGroup(mainline.lv2_id)} selectedEvent={selectedEvent} onEventSelect={onEventSelect} eventFollowStatus={eventFollowStatus} onToggleFollow={onToggleFollow} borderColor={borderColor} /> ))} ); } ); MainlineTimelineViewComponent.displayName = "MainlineTimelineView"; const MainlineTimelineView = React.memo(MainlineTimelineViewComponent); export default MainlineTimelineView;