From 897067a94e1a2da5014b36aea9766f623d5c6f36 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Mon, 22 Dec 2025 12:14:57 +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 --- src/mocks/handlers/event.js | 79 ++- .../layouts/MainlineTimelineView.js | 454 ++++++++++-------- .../SearchFilters/CompactSearchBox.css | 27 ++ 3 files changed, 343 insertions(+), 217 deletions(-) diff --git a/src/mocks/handlers/event.js b/src/mocks/handlers/event.js index 7306e04f..32952a50 100644 --- a/src/mocks/handlers/event.js +++ b/src/mocks/handlers/event.js @@ -1588,7 +1588,7 @@ export const eventHandlers = [ // ==================== 主线模式相关 ==================== - // 获取按主线(lv2概念)分组的事件列表 + // 获取按主线(lv1/lv2概念)分组的事件列表 http.get('/api/events/mainline', async ({ request }) => { await delay(500); @@ -1596,8 +1596,10 @@ export const eventHandlers = [ const recentDays = parseInt(url.searchParams.get('recent_days') || '7', 10); const importance = url.searchParams.get('importance') || 'all'; const limitPerMainline = parseInt(url.searchParams.get('limit') || '20', 10); + // 分组方式: 'lv1' | 'lv2' | 具体的 lv2_id(如 'L2_AI_INFRA') + const groupBy = url.searchParams.get('group_by') || 'lv2'; - console.log('[Mock Event] 获取主线数据:', { recentDays, importance, limitPerMainline }); + console.log('[Mock Event] 获取主线数据:', { recentDays, importance, limitPerMainline, groupBy }); try { // 生成 mock 事件数据 @@ -1605,21 +1607,32 @@ export const eventHandlers = [ // 定义 lv2 主线分类 const mainlineDefinitions = [ - { lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['算力', 'AI', '人工智能', 'CPO', 'PCB', '光模块', '大模型', '智能体'] }, - { lv2_id: 'L2_SEMICONDUCTOR', lv2_name: '半导体 (设计/制造/封测)', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['半导体', '芯片', '光刻', 'IC', '晶圆', '封装', '测试'] }, - { lv2_id: 'L2_ROBOT', lv2_name: '机器人 (人形机器人/工业机器人)', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['机器人', '人形', '自动化', '工业机器人', '具身智能'] }, - { lv2_id: 'L2_CONSUMER_ELEC', lv2_name: '消费电子 (手机/XR/可穿戴)', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['手机', 'XR', 'VR', 'AR', '可穿戴', '华为', '苹果', '消费电子'] }, - { lv2_id: 'L2_NEW_ENERGY', lv2_name: '新能源 (光伏/储能/电池)', lv1_name: '新能源与环保', keywords: ['光伏', '储能', '电池', '锂电', '新能源', '风电', '清洁能源'] }, - { lv2_id: 'L2_EV', lv2_name: '新能源汽车 (整车/零部件)', lv1_name: '新能源与环保', keywords: ['新能源汽车', '电动车', '智能驾驶', '汽车', '整车', '零部件'] }, - { lv2_id: 'L2_LOW_ALTITUDE', lv2_name: '低空经济 (无人机/eVTOL)', lv1_name: '先进制造', keywords: ['低空', '无人机', 'eVTOL', '飞行', '空域'] }, - { lv2_id: 'L2_MILITARY', lv2_name: '军工 (航空航天/国防)', lv1_name: '先进制造', keywords: ['军工', '国防', '航空', '航天', '卫星', '导弹'] }, - { lv2_id: 'L2_PHARMA', lv2_name: '医药医疗 (创新药/器械)', lv1_name: '医药健康', keywords: ['医药', '医疗', '创新药', '器械', '生物', 'CXO'] }, - { lv2_id: 'L2_FINANCE', lv2_name: '金融 (银行/券商/保险)', lv1_name: '金融', keywords: ['金融', '银行', '券商', '保险', '证券'] }, + { lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['算力', 'AI', '人工智能', 'CPO', 'PCB', '光模块', '大模型', '智能体'] }, + { lv2_id: 'L2_SEMICONDUCTOR', lv2_name: '半导体 (设计/制造/封测)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['半导体', '芯片', '光刻', 'IC', '晶圆', '封装', '测试'] }, + { lv2_id: 'L2_ROBOT', lv2_name: '机器人 (人形机器人/工业机器人)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['机器人', '人形', '自动化', '工业机器人', '具身智能'] }, + { lv2_id: 'L2_CONSUMER_ELEC', lv2_name: '消费电子 (手机/XR/可穿戴)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['手机', 'XR', 'VR', 'AR', '可穿戴', '华为', '苹果', '消费电子'] }, + { lv2_id: 'L2_TELECOM', lv2_name: '通信、互联网与软件', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['通信', '5G', '6G', '互联网', '软件', '云计算', 'SaaS', '数字化'] }, + { lv2_id: 'L2_NEW_ENERGY', lv2_name: '新能源 (光伏/储能/电池)', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车 (New Energy & Auto)', keywords: ['光伏', '储能', '电池', '锂电', '新能源', '风电', '清洁能源'] }, + { lv2_id: 'L2_EV', lv2_name: '智能网联汽车', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车 (New Energy & Auto)', keywords: ['新能源汽车', '电动车', '智能驾驶', '汽车', '整车', '零部件', '智能网联'] }, + { lv2_id: 'L2_LOW_ALTITUDE', lv2_name: '低空经济 (无人机/eVTOL)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['低空', '无人机', 'eVTOL', '飞行', '空域'] }, + { lv2_id: 'L2_MILITARY', lv2_name: '军工 (航空航天/国防)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['军工', '国防', '航空', '航天', '卫星', '导弹'] }, + { lv2_id: 'L2_PHARMA', lv2_name: '医药医疗 (创新药/器械)', lv1_id: 'L1_PHARMA', lv1_name: '医药健康', keywords: ['医药', '医疗', '创新药', '器械', '生物', 'CXO'] }, + { lv2_id: 'L2_FINANCE', lv2_name: '金融 (银行/券商/保险)', lv1_id: 'L1_FINANCE', lv1_name: '金融', keywords: ['金融', '银行', '券商', '保险', '证券'] }, ]; + // 生成 lv2 选项列表供前端下拉框使用 + const lv2Options = mainlineDefinitions.map(m => ({ + lv2_id: m.lv2_id, + lv2_name: m.lv2_name, + lv1_name: m.lv1_name, + })); + // 按主线分组事件 const mainlineGroups = {}; + // 判断是否为具体的 lv2_id + const isSpecificLv2 = groupBy.startsWith('L2_'); + allEvents.forEach(event => { // 从事件的 keywords (related_concepts) 中查找匹配的主线 const keywords = event.keywords || event.related_concepts || []; @@ -1631,17 +1644,44 @@ export const eventHandlers = [ mainlineDefinitions.forEach(mainline => { const matched = mainline.keywords.some(kw => textToMatch.includes(kw.toLowerCase())); if (matched) { - if (!mainlineGroups[mainline.lv2_id]) { - mainlineGroups[mainline.lv2_id] = { + // 根据分组方式决定分组键 + let groupKey, groupData; + + if (isSpecificLv2) { + // 具体 lv2 概念:只显示该概念下的事件,不分组 + if (mainline.lv2_id !== groupBy) return; + groupKey = mainline.lv2_id; + groupData = { + lv2_id: mainline.lv2_id, + lv2_name: mainline.lv2_name, + lv1_name: mainline.lv1_name, + events: [] + }; + } else if (groupBy === 'lv1') { + // 按一级概念分组 + groupKey = mainline.lv1_id; + groupData = { + lv1_id: mainline.lv1_id, + lv1_name: mainline.lv1_name, + events: [] + }; + } else { + // 默认按二级概念分组 + groupKey = mainline.lv2_id; + groupData = { lv2_id: mainline.lv2_id, lv2_name: mainline.lv2_name, lv1_name: mainline.lv1_name, events: [] }; } + + if (!mainlineGroups[groupKey]) { + mainlineGroups[groupKey] = groupData; + } // 避免重复添加 - if (!mainlineGroups[mainline.lv2_id].events.find(e => e.id === event.id)) { - mainlineGroups[mainline.lv2_id].events.push(event); + if (!mainlineGroups[groupKey].events.find(e => e.id === event.id)) { + mainlineGroups[groupKey].events.push(event); } } }); @@ -1665,7 +1705,8 @@ export const eventHandlers = [ console.log('[Mock Event] 主线数据生成完成:', { mainlineCount: mainlines.length, totalEvents: allEvents.length, - ungroupedCount + ungroupedCount, + groupBy }); return HttpResponse.json({ @@ -1674,7 +1715,9 @@ export const eventHandlers = [ mainlines, total_events: allEvents.length, mainline_count: mainlines.length, - ungrouped_count: ungroupedCount + ungrouped_count: ungroupedCount, + group_by: groupBy, + lv2_options: lv2Options, } }); } catch (error) { diff --git a/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js b/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js index ceb662b2..ced08b53 100644 --- a/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js +++ b/src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js @@ -1,5 +1,5 @@ // src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js -// 主线时间轴布局组件 - 按 lv2 概念分组展示事件(横向滚动布局) +// 主线时间轴布局组件 - 按 lv1/lv2 概念分组展示事件(横向滚动布局) import React, { useState, @@ -22,16 +22,13 @@ import { IconButton, Tooltip, Button, - SimpleGrid, } from "@chakra-ui/react"; -import { - ChevronDownIcon, - ChevronUpIcon, - RepeatIcon, -} from "@chakra-ui/icons"; +import { ChevronDownIcon, ChevronUpIcon, RepeatIcon } from "@chakra-ui/icons"; import { FiTrendingUp, FiZap } from "react-icons/fi"; +import { Select } from "antd"; import MiniEventCard from "../../EventCard/MiniEventCard"; import { getApiBase } from "@utils/apiConfig"; +import "../../SearchFilters/CompactSearchBox.css"; // 固定深色主题颜色 const COLORS = { @@ -53,203 +50,215 @@ const EVENTS_PER_LOAD = 12; /** * 单个主线卡片组件 - 支持懒加载 */ -const MainlineCard = React.memo(({ - mainline, - colorScheme, - isExpanded, - onToggle, - selectedEvent, - onEventSelect, -}) => { - // 懒加载状态 - const [displayCount, setDisplayCount] = useState(EVENTS_PER_LOAD); - const [isLoadingMore, setIsLoadingMore] = useState(false); +const MainlineCard = React.memo( + ({ + mainline, + colorScheme, + isExpanded, + onToggle, + selectedEvent, + onEventSelect, + }) => { + // 懒加载状态 + const [displayCount, setDisplayCount] = useState(EVENTS_PER_LOAD); + const [isLoadingMore, setIsLoadingMore] = useState(false); - // 重置显示数量当折叠时 - useEffect(() => { - if (!isExpanded) { - setDisplayCount(EVENTS_PER_LOAD); - } - }, [isExpanded]); + // 重置显示数量当折叠时 + useEffect(() => { + if (!isExpanded) { + setDisplayCount(EVENTS_PER_LOAD); + } + }, [isExpanded]); - // 当前显示的事件 - const displayedEvents = useMemo(() => { - return mainline.events.slice(0, displayCount); - }, [mainline.events, displayCount]); + // 当前显示的事件 + const displayedEvents = useMemo(() => { + return mainline.events.slice(0, displayCount); + }, [mainline.events, displayCount]); - // 是否还有更多 - const hasMore = displayCount < mainline.events.length; + // 是否还有更多 + const hasMore = displayCount < mainline.events.length; - // 加载更多 - const loadMore = useCallback((e) => { - e.stopPropagation(); - setIsLoadingMore(true); - setTimeout(() => { - setDisplayCount(prev => Math.min(prev + EVENTS_PER_LOAD, mainline.events.length)); - setIsLoadingMore(false); - }, 50); - }, [mainline.events.length]); + // 加载更多 + const loadMore = useCallback( + (e) => { + e.stopPropagation(); + setIsLoadingMore(true); + setTimeout(() => { + setDisplayCount((prev) => + Math.min(prev + EVENTS_PER_LOAD, mainline.events.length) + ); + setIsLoadingMore(false); + }, 50); + }, + [mainline.events.length] + ); - return ( - - {/* 卡片头部 */} - - - - - {mainline.lv2_name || "其他"} - - {/* 涨跌幅显示 - 在概念名称旁边 */} - {mainline.avg_change_pct != null && ( + {/* 卡片头部 */} + + + = 0 ? "#fc8181" : "#68d391"} + fontSize="sm" + color={COLORS.textColor} + noOfLines={1} + flex={1} + > + {mainline.lv2_name || mainline.lv1_name || "其他"} + + {/* 涨跌幅显示 - 在概念名称旁边 */} + {mainline.avg_change_pct != null && ( + = 0 ? "#fc8181" : "#68d391"} + flexShrink={0} + > + {mainline.avg_change_pct >= 0 ? "+" : ""} + {mainline.avg_change_pct.toFixed(2)}% + + )} + - {mainline.avg_change_pct >= 0 ? "+" : ""} - {mainline.avg_change_pct.toFixed(2)}% - - )} - - {mainline.event_count} - - - {mainline.lv1_name && ( - - {mainline.lv1_name} - - )} - - - - - {/* 事件列表区域 */} - {isExpanded ? ( - - {/* 事件网格 - 2列布局 */} - - {displayedEvents.map((event) => ( - - ))} - - - {/* 加载更多按钮 */} - {hasMore && ( - - )} - - ) : ( - /* 折叠时显示简要信息 */ - - - {mainline.events.slice(0, 4).map((event) => ( + {mainline.event_count} + + + {/* 只有当同时存在 lv1_name 和 lv2_name 时才显示副标题 */} + {mainline.lv1_name && mainline.lv2_name && ( { - e.stopPropagation(); - onEventSelect?.(event); - }} > - • {event.title} - - ))} - {mainline.events.length > 4 && ( - - ... 还有 {mainline.events.length - 4} 条 + {mainline.lv1_name} )} - - )} - - ); -}); + + + + {/* 事件列表区域 */} + {isExpanded ? ( + + {/* 事件列表 - 单列布局 */} + + {displayedEvents.map((event) => ( + + ))} + + + {/* 加载更多按钮 */} + {hasMore && ( + + )} + + ) : ( + /* 折叠时显示简要信息 */ + + + {mainline.events.slice(0, 4).map((event) => ( + { + e.stopPropagation(); + onEventSelect?.(event); + }} + > + • {event.title} + + ))} + {mainline.events.length > 4 && ( + + ... 还有 {mainline.events.length - 4} 条 + + )} + + + )} + + ); + } +); MainlineCard.displayName = "MainlineCard"; @@ -274,6 +283,10 @@ const MainlineTimelineViewComponent = forwardRef( const [error, setError] = useState(null); const [mainlineData, setMainlineData] = useState(null); const [expandedGroups, setExpandedGroups] = useState({}); + // 概念级别选择: 'lv1' | 'lv2' | 具体的 lv2_id + const [groupBy, setGroupBy] = useState("lv2"); + // lv2 概念选项(从 API 获取) + const [lv2Options, setLv2Options] = useState([]); // 根据主线类型获取配色 const getColorScheme = useCallback((lv2Name) => { @@ -363,6 +376,8 @@ const MainlineTimelineViewComponent = forwardRef( params.append("recent_days", filters.recent_days); if (filters.importance && filters.importance !== "all") params.append("importance", filters.importance); + // 添加分组方式参数 + params.append("group_by", groupBy); const url = `${apiBase}/api/events/mainline?${params.toString()}`; console.log("[MainlineTimelineView] 🔄 请求主线数据:", url); @@ -377,6 +392,7 @@ const MainlineTimelineViewComponent = forwardRef( success: result.success, mainlineCount: result.data?.mainlines?.length, totalEvents: result.data?.total_events, + groupBy: result.data?.group_by, }); if (result.success) { @@ -390,10 +406,15 @@ const MainlineTimelineViewComponent = forwardRef( mainlines: sortedMainlines, }); + // 保存 lv2 选项供下拉框使用 + if (result.data.lv2_options) { + setLv2Options(result.data.lv2_options); + } + // 初始化展开状态(默认全部展开) const initialExpanded = {}; sortedMainlines.forEach((mainline) => { - initialExpanded[mainline.lv2_id] = true; + initialExpanded[mainline.lv2_id || mainline.lv1_id] = true; }); setExpandedGroups(initialExpanded); } else { @@ -405,7 +426,7 @@ const MainlineTimelineViewComponent = forwardRef( } finally { setLoading(false); } - }, [display, filters.recent_days, filters.importance]); + }, [display, filters.recent_days, filters.importance, groupBy]); // 初始加载 & 筛选变化时刷新 useEffect(() => { @@ -535,7 +556,37 @@ const MainlineTimelineViewComponent = forwardRef( )} - + + {/* 概念级别选择器 */} +