fix(mock): 修复主线数据不显示问题
- 调整 MSW handler 顺序,确保 /api/events/mainline 在 :eventId 之前匹配 - 修复 generateDynamicNewsEvents 函数调用参数顺序错误 - 添加主线事件模板,确保生成的事件能匹配主线关键词 - 删除重复的 mainline handler 代码 - 清理调试日志
This commit is contained in:
@@ -1142,6 +1142,138 @@ function generateTransmissionChain(industry, index) {
|
|||||||
return { nodes, edges };
|
return { nodes, edges };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 主线事件标题模板 - 确保生成的事件能够匹配主线定义的关键词
|
||||||
|
// 每个主线定义至少有 2-3 个事件模板,确保数据充足
|
||||||
|
const mainlineEventTemplates = [
|
||||||
|
// ==================== TMT (科技/媒体/通信) ====================
|
||||||
|
|
||||||
|
// AI基础设施 - 算力/芯片 (L3_AI_CHIP)
|
||||||
|
{ title: '英伟达发布新一代GPU,AI算力大幅提升', keywords: ['算力', 'AI芯片', 'GPU', '英伟达'], industry: '人工智能' },
|
||||||
|
{ title: '华为昇腾芯片出货量创新高,国产AI算力加速', keywords: ['华为昇腾', 'AI芯片', '算力'], industry: '人工智能' },
|
||||||
|
{ title: '寒武纪发布新一代AI训练芯片,性能提升50%', keywords: ['寒武纪', 'AI芯片', '算力'], industry: '人工智能' },
|
||||||
|
{ title: 'AI芯片需求激增,GPU供不应求价格上涨', keywords: ['AI芯片', 'GPU', '算力'], industry: '人工智能' },
|
||||||
|
|
||||||
|
// AI基础设施 - 服务器与数据中心 (L3_AI_SERVER)
|
||||||
|
{ title: '智算中心建设加速,多地启动算力基础设施项目', keywords: ['智算中心', '算力', '数据中心'], industry: '人工智能' },
|
||||||
|
{ title: '液冷技术成数据中心标配,服务器散热升级', keywords: ['液冷', '数据中心', '服务器'], industry: '人工智能' },
|
||||||
|
{ title: 'AI服务器订单暴增,数据中心扩容需求旺盛', keywords: ['服务器', '数据中心', '智算中心'], industry: '人工智能' },
|
||||||
|
|
||||||
|
// AI基础设施 - 光通信 (L3_OPTICAL)
|
||||||
|
{ title: 'CPO技术迎来突破,光模块成本大幅下降', keywords: ['CPO', '光模块', '光通信'], industry: '通信' },
|
||||||
|
{ title: '800G光模块量产加速,AI训练网络升级', keywords: ['光模块', '光通信', '光芯片'], industry: '通信' },
|
||||||
|
{ title: '光芯片技术突破,CPO方案渗透率提升', keywords: ['光芯片', 'CPO', '光通信', '光模块'], industry: '通信' },
|
||||||
|
|
||||||
|
// AI基础设施 - PCB与封装 (L3_PCB)
|
||||||
|
{ title: 'AI PCB需求激增,高多层板产能紧张', keywords: ['PCB', 'AI PCB', '封装'], industry: '电子' },
|
||||||
|
{ title: '先进封装技术突破,PCB产业链升级', keywords: ['封装', 'PCB'], industry: '电子' },
|
||||||
|
|
||||||
|
// AI应用与大模型 (L3_AI_APP)
|
||||||
|
{ title: 'DeepSeek发布最新大模型,推理能力超越GPT-4', keywords: ['DeepSeek', '大模型', 'AI', '人工智能'], industry: '人工智能' },
|
||||||
|
{ title: 'KIMI月活突破1亿,国产大模型竞争白热化', keywords: ['KIMI', '大模型', 'AI', '人工智能'], industry: '人工智能' },
|
||||||
|
{ title: 'ChatGPT新版本发布,AI Agent智能体能力升级', keywords: ['ChatGPT', '大模型', '智能体', 'AI'], industry: '人工智能' },
|
||||||
|
{ title: '人工智能+医疗深度融合,AI辅助诊断准确率超90%', keywords: ['人工智能', 'AI', '医疗'], industry: '人工智能' },
|
||||||
|
{ title: '多模态大模型技术突破,AI应用场景扩展', keywords: ['大模型', 'AI', '人工智能'], industry: '人工智能' },
|
||||||
|
|
||||||
|
// 半导体 - 芯片设计 (L3_CHIP_DESIGN)
|
||||||
|
{ title: '芯片设计企业扩产,IC设计产能大幅提升', keywords: ['芯片设计', 'IC设计', '半导体'], industry: '半导体' },
|
||||||
|
{ title: '国产芯片设计工具取得突破,EDA软件自主可控', keywords: ['芯片设计', 'IC设计', '半导体'], industry: '半导体' },
|
||||||
|
|
||||||
|
// 半导体 - 芯片制造 (L3_CHIP_MFG)
|
||||||
|
{ title: '中芯国际N+1工艺量产,芯片制造技术再突破', keywords: ['中芯国际', '芯片制造', '晶圆'], industry: '半导体' },
|
||||||
|
{ title: '国产光刻机实现技术突破,半导体设备自主可控', keywords: ['光刻', '半导体', '芯片制造'], industry: '半导体' },
|
||||||
|
{ title: '晶圆产能利用率回升,半导体行业景气度上行', keywords: ['晶圆', '半导体', '芯片制造'], industry: '半导体' },
|
||||||
|
|
||||||
|
// 机器人 - 人形机器人 (L3_HUMANOID)
|
||||||
|
{ title: '特斯拉人形机器人Optimus量产提速,成本降至2万美元', keywords: ['人形机器人', '特斯拉机器人', '具身智能'], industry: '人工智能' },
|
||||||
|
{ title: '具身智能迎来突破,人形机器人商业化加速', keywords: ['具身智能', '人形机器人', '机器人'], industry: '人工智能' },
|
||||||
|
{ title: '人形机器人产业链爆发,核心零部件需求激增', keywords: ['人形机器人', '具身智能'], industry: '人工智能' },
|
||||||
|
|
||||||
|
// 机器人 - 工业机器人 (L3_INDUSTRIAL_ROBOT)
|
||||||
|
{ title: '工业机器人出货量同比增长30%,自动化渗透率提升', keywords: ['工业机器人', '自动化', '机器人'], industry: '机械' },
|
||||||
|
{ title: '智能制造升级,机器人自动化需求持续增长', keywords: ['机器人', '自动化', '工业机器人'], industry: '机械' },
|
||||||
|
|
||||||
|
// 消费电子 - 智能手机 (L3_MOBILE)
|
||||||
|
{ title: '华为Mate系列销量火爆,折叠屏手机市场爆发', keywords: ['华为', '手机', '折叠屏'], industry: '消费电子' },
|
||||||
|
{ title: '小米可穿戴设备出货量全球第一,智能手表市场扩张', keywords: ['小米', '可穿戴', '手机'], industry: '消费电子' },
|
||||||
|
{ title: '手机市场复苏,5G手机换机潮来临', keywords: ['手机', '华为', '小米'], industry: '消费电子' },
|
||||||
|
|
||||||
|
// 消费电子 - XR与可穿戴 (L3_XR)
|
||||||
|
{ title: '苹果Vision Pro销量不及预期,XR设备面临挑战', keywords: ['苹果', 'Vision Pro', 'XR', 'MR'], industry: '消费电子' },
|
||||||
|
{ title: 'AR眼镜成新风口,VR/AR设备渗透率提升', keywords: ['AR', 'VR', 'XR'], industry: '消费电子' },
|
||||||
|
{ title: 'Meta发布新一代VR头显,XR市场竞争加剧', keywords: ['VR', 'XR', 'MR', '可穿戴'], industry: '消费电子' },
|
||||||
|
|
||||||
|
// 通信 - 5G/6G通信 (L3_5G)
|
||||||
|
{ title: '5G基站建设加速,运营商资本开支超预期', keywords: ['5G', '基站', '通信'], industry: '通信' },
|
||||||
|
{ title: '6G技术标准制定启动,下一代通信网络布局', keywords: ['6G', '5G', '通信'], industry: '通信' },
|
||||||
|
|
||||||
|
// 通信 - 云计算与软件 (L3_CLOUD)
|
||||||
|
{ title: '云计算市场规模突破万亿,SaaS企业业绩增长', keywords: ['云计算', 'SaaS', '软件', '互联网'], industry: '互联网' },
|
||||||
|
{ title: '数字化转型加速,企业级软件需求旺盛', keywords: ['数字化', '软件', '互联网'], industry: '互联网' },
|
||||||
|
{ title: '国产软件替代加速,信创产业迎来发展机遇', keywords: ['软件', '数字化', '互联网'], industry: '互联网' },
|
||||||
|
|
||||||
|
// ==================== 新能源与智能汽车 ====================
|
||||||
|
|
||||||
|
// 新能源 - 光伏 (L3_PV)
|
||||||
|
{ title: '光伏装机量创新高,太阳能发电成本持续下降', keywords: ['光伏', '太阳能', '硅片', '组件'], industry: '新能源' },
|
||||||
|
{ title: '光伏组件价格企稳,行业出清接近尾声', keywords: ['光伏', '组件', '硅片'], industry: '新能源' },
|
||||||
|
{ title: '分布式光伏爆发,太阳能产业链受益', keywords: ['光伏', '太阳能'], industry: '新能源' },
|
||||||
|
|
||||||
|
// 新能源 - 储能与电池 (L3_STORAGE)
|
||||||
|
{ title: '储能市场爆发式增长,电池需求大幅提升', keywords: ['储能', '电池', '锂电', '新能源'], industry: '新能源' },
|
||||||
|
{ title: '固态电池技术突破,新能源汽车续航大幅提升', keywords: ['固态电池', '电池', '锂电', '新能源'], industry: '新能源' },
|
||||||
|
{ title: '钠电池产业化加速,储能成本有望大幅下降', keywords: ['电池', '储能', '新能源'], industry: '新能源' },
|
||||||
|
|
||||||
|
// 智能汽车 - 新能源整车 (L3_EV_OEM)
|
||||||
|
{ title: '比亚迪月销量突破50万辆,新能源汽车市占率第一', keywords: ['比亚迪', '新能源汽车', '电动车'], industry: '新能源' },
|
||||||
|
{ title: '新能源整车出口创新高,中国汽车品牌走向全球', keywords: ['新能源汽车', '整车', '电动车'], industry: '新能源' },
|
||||||
|
{ title: '电动车价格战持续,新能源汽车渗透率突破50%', keywords: ['电动车', '新能源汽车', '整车'], industry: '新能源' },
|
||||||
|
|
||||||
|
// 智能汽车 - 智能驾驶 (L3_AUTO_DRIVE)
|
||||||
|
{ title: '特斯拉FSD自动驾驶进入中国市场,智能驾驶加速', keywords: ['特斯拉', '自动驾驶', '智能驾驶', '智能网联'], industry: '新能源' },
|
||||||
|
{ title: '车路协同试点扩大,智能网联汽车基建提速', keywords: ['车路协同', '智能网联', '智能驾驶'], industry: '新能源' },
|
||||||
|
{ title: 'L3级自动驾驶获批,智能驾驶产业化加速', keywords: ['自动驾驶', '智能驾驶', '智能网联'], industry: '新能源' },
|
||||||
|
|
||||||
|
// ==================== 先进制造 ====================
|
||||||
|
|
||||||
|
// 低空经济 - 无人机 (L3_DRONE)
|
||||||
|
{ title: '低空经济政策密集出台,无人机产业迎来风口', keywords: ['低空', '无人机', '空域'], industry: '航空' },
|
||||||
|
{ title: '无人机应用场景拓展,低空经济市场规模扩大', keywords: ['无人机', '低空', '空域'], industry: '航空' },
|
||||||
|
|
||||||
|
// 低空经济 - eVTOL (L3_EVTOL)
|
||||||
|
{ title: 'eVTOL飞行汽车完成首飞,空中出租车商业化在即', keywords: ['eVTOL', '飞行汽车', '空中出租车'], industry: '航空' },
|
||||||
|
{ title: '飞行汽车获适航认证,eVTOL商业运营启动', keywords: ['飞行汽车', 'eVTOL', '空中出租车'], industry: '航空' },
|
||||||
|
|
||||||
|
// 军工 - 航空航天 (L3_AEROSPACE)
|
||||||
|
{ title: '商业航天发射成功,卫星互联网建设加速', keywords: ['航天', '卫星', '火箭'], industry: '军工' },
|
||||||
|
{ title: '航空发动机国产化取得突破,航空产业链升级', keywords: ['航空', '军工'], industry: '军工' },
|
||||||
|
{ title: '卫星通信需求爆发,航天发射频次创新高', keywords: ['卫星', '航天', '火箭'], industry: '军工' },
|
||||||
|
|
||||||
|
// 军工 - 国防军工 (L3_DEFENSE)
|
||||||
|
{ title: '军工订单饱满,国防装备现代化提速', keywords: ['军工', '国防', '军工装备'], industry: '军工' },
|
||||||
|
{ title: '国防预算增长,军工装备需求持续提升', keywords: ['国防', '军工', '军工装备', '导弹'], industry: '军工' },
|
||||||
|
|
||||||
|
// ==================== 医药健康 ====================
|
||||||
|
|
||||||
|
// 医药 - 创新药 (L3_DRUG)
|
||||||
|
{ title: '创新药获批上市,医药板块迎来业绩兑现', keywords: ['创新药', '医药', '生物'], industry: '医药' },
|
||||||
|
{ title: 'CXO订单持续增长,医药研发外包景气度高', keywords: ['CXO', '医药', '生物'], industry: '医药' },
|
||||||
|
{ title: '生物医药融资回暖,创新药研发管线丰富', keywords: ['医药', '生物', '创新药'], industry: '医药' },
|
||||||
|
|
||||||
|
// 医药 - 医疗器械 (L3_DEVICE)
|
||||||
|
{ title: '医疗器械集采扩面,国产替代加速', keywords: ['医疗器械', '医疗', '器械'], industry: '医药' },
|
||||||
|
{ title: '高端医疗设备国产化突破,医疗器械出口增长', keywords: ['医疗器械', '医疗', '器械'], industry: '医药' },
|
||||||
|
|
||||||
|
// ==================== 金融 ====================
|
||||||
|
|
||||||
|
// 金融 - 银行 (L3_BANK)
|
||||||
|
{ title: '银行净息差企稳,金融板块估值修复', keywords: ['银行', '金融'], industry: '金融' },
|
||||||
|
{ title: '银行业绩超预期,金融股迎来价值重估', keywords: ['银行', '金融'], industry: '金融' },
|
||||||
|
|
||||||
|
// 金融 - 券商 (L3_BROKER)
|
||||||
|
{ title: '券商业绩大幅增长,证券板块活跃', keywords: ['券商', '证券', '金融'], industry: '金融' },
|
||||||
|
{ title: 'A股成交量放大,证券行业业绩弹性显现', keywords: ['证券', '券商', '金融'], industry: '金融' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成动态新闻事件(实时要闻·动态追踪专用)
|
* 生成动态新闻事件(实时要闻·动态追踪专用)
|
||||||
* @param {Object} timeRange - 时间范围 { startTime, endTime }
|
* @param {Object} timeRange - 时间范围 { startTime, endTime }
|
||||||
@@ -1166,7 +1298,10 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
|
|||||||
const timeSpan = endTime.getTime() - startTime.getTime();
|
const timeSpan = endTime.getTime() - startTime.getTime();
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const industry = industries[i % industries.length];
|
// 使用主线事件模板生成事件,确保能匹配主线关键词
|
||||||
|
const templateIndex = i % mainlineEventTemplates.length;
|
||||||
|
const template = mainlineEventTemplates[templateIndex];
|
||||||
|
const industry = template.industry;
|
||||||
const imp = importanceLevels[i % importanceLevels.length];
|
const imp = importanceLevels[i % importanceLevels.length];
|
||||||
const eventType = eventTypes[i % eventTypes.length];
|
const eventType = eventTypes[i % eventTypes.length];
|
||||||
|
|
||||||
@@ -1217,11 +1352,33 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用模板标题,并生成包含模板关键词的 keywords 数组
|
||||||
|
const eventTitle = template.title;
|
||||||
|
const eventDescription = generateEventDescription(industry, imp, i);
|
||||||
|
|
||||||
|
// 生成关键词对象数组,包含模板中的关键词
|
||||||
|
const templateKeywords = template.keywords.map((kw, idx) => ({
|
||||||
|
concept: kw,
|
||||||
|
stock_count: 10 + Math.floor(Math.random() * 20),
|
||||||
|
score: parseFloat((0.7 + Math.random() * 0.25).toFixed(2)),
|
||||||
|
description: `${kw}相关概念,市场关注度较高`,
|
||||||
|
price_info: {
|
||||||
|
avg_change_pct: parseFloat((Math.random() * 15 - 5).toFixed(2))
|
||||||
|
},
|
||||||
|
match_type: ['hybrid_knn', 'keyword', 'semantic'][idx % 3]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 合并模板关键词和行业关键词
|
||||||
|
const mergedKeywords = [
|
||||||
|
...templateKeywords,
|
||||||
|
...generateKeywords(industry, i).slice(0, 2)
|
||||||
|
];
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
id: `dynamic_${i + 1}`,
|
id: `dynamic_${i + 1}`,
|
||||||
title: generateEventTitle(industry, i),
|
title: eventTitle,
|
||||||
description: generateEventDescription(industry, imp, i),
|
description: eventDescription,
|
||||||
content: generateEventDescription(industry, imp, i),
|
content: eventDescription,
|
||||||
event_type: eventType,
|
event_type: eventType,
|
||||||
importance: imp,
|
importance: imp,
|
||||||
status: 'published',
|
status: 'published',
|
||||||
@@ -1234,7 +1391,9 @@ export function generateDynamicNewsEvents(timeRange = null, count = 30) {
|
|||||||
related_avg_chg: parseFloat(relatedAvgChg),
|
related_avg_chg: parseFloat(relatedAvgChg),
|
||||||
related_max_chg: parseFloat(relatedMaxChg),
|
related_max_chg: parseFloat(relatedMaxChg),
|
||||||
related_week_chg: parseFloat(relatedWeekChg),
|
related_week_chg: parseFloat(relatedWeekChg),
|
||||||
keywords: generateKeywords(industry, i),
|
expectation_surprise_score: Math.floor(Math.random() * 60) + 30, // 30-90 超预期得分
|
||||||
|
keywords: mergedKeywords,
|
||||||
|
related_concepts: mergedKeywords, // 添加 related_concepts 字段,兼容主线匹配逻辑
|
||||||
is_ai_generated: i % 3 === 0, // 33% 的事件是AI生成
|
is_ai_generated: i % 3 === 0, // 33% 的事件是AI生成
|
||||||
industry: industry,
|
industry: industry,
|
||||||
related_stocks: relatedStocks,
|
related_stocks: relatedStocks,
|
||||||
|
|||||||
@@ -257,6 +257,159 @@ export const eventHandlers = [
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// ==================== 主线模式相关(必须在 :eventId 之前,否则会被通配符匹配)====================
|
||||||
|
|
||||||
|
// 获取按主线(lv1/lv2/lv3概念)分组的事件列表
|
||||||
|
http.get('/api/events/mainline', async ({ request }) => {
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
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);
|
||||||
|
const groupBy = url.searchParams.get('group_by') || 'lv2';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 生成 mock 事件数据 - 第一个参数是 timeRange(null 表示默认24小时),第二个参数是 count
|
||||||
|
const allEvents = generateDynamicNewsEvents(null, 100);
|
||||||
|
|
||||||
|
const mainlineDefinitions = [
|
||||||
|
{ lv3_id: 'L3_AI_CHIP', lv3_name: 'AI芯片与算力', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['算力', 'AI芯片', 'GPU', '英伟达', '华为昇腾', '寒武纪'] },
|
||||||
|
{ lv3_id: 'L3_AI_SERVER', lv3_name: '服务器与数据中心', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['服务器', '数据中心', '智算中心', '液冷'] },
|
||||||
|
{ lv3_id: 'L3_OPTICAL', lv3_name: '光通信与CPO', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['CPO', '光通信', '光模块', '光芯片'] },
|
||||||
|
{ lv3_id: 'L3_PCB', lv3_name: 'PCB与封装', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['PCB', '封装', 'AI PCB'] },
|
||||||
|
{ lv3_id: 'L3_AI_APP', lv3_name: 'AI应用与大模型', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['大模型', '智能体', 'AI', '人工智能', 'DeepSeek', 'KIMI', 'ChatGPT'] },
|
||||||
|
{ lv3_id: 'L3_CHIP_DESIGN', lv3_name: '芯片设计', lv2_id: 'L2_SEMICONDUCTOR', lv2_name: '半导体 (设计/制造/封测)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['芯片设计', '半导体', 'IC设计'] },
|
||||||
|
{ lv3_id: 'L3_CHIP_MFG', lv3_name: '芯片制造', lv2_id: 'L2_SEMICONDUCTOR', lv2_name: '半导体 (设计/制造/封测)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['晶圆', '光刻', '芯片制造', '中芯国际'] },
|
||||||
|
{ lv3_id: 'L3_HUMANOID', lv3_name: '人形机器人', lv2_id: 'L2_ROBOT', lv2_name: '机器人 (人形机器人/工业机器人)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['人形机器人', '具身智能', '特斯拉机器人'] },
|
||||||
|
{ lv3_id: 'L3_INDUSTRIAL_ROBOT', lv3_name: '工业机器人', lv2_id: 'L2_ROBOT', lv2_name: '机器人 (人形机器人/工业机器人)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['工业机器人', '自动化', '机器人'] },
|
||||||
|
{ lv3_id: 'L3_MOBILE', lv3_name: '智能手机', lv2_id: 'L2_CONSUMER_ELEC', lv2_name: '消费电子 (手机/XR/可穿戴)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['手机', '华为', '苹果', '小米', '折叠屏'] },
|
||||||
|
{ lv3_id: 'L3_XR', lv3_name: 'XR与可穿戴', lv2_id: 'L2_CONSUMER_ELEC', lv2_name: '消费电子 (手机/XR/可穿戴)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['XR', 'VR', 'AR', '可穿戴', 'MR', 'Vision Pro'] },
|
||||||
|
{ lv3_id: 'L3_5G', lv3_name: '5G/6G通信', lv2_id: 'L2_TELECOM', lv2_name: '通信、互联网与软件', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['5G', '6G', '通信', '基站'] },
|
||||||
|
{ lv3_id: 'L3_CLOUD', lv3_name: '云计算与软件', lv2_id: 'L2_TELECOM', lv2_name: '通信、互联网与软件', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['云计算', '软件', 'SaaS', '互联网', '数字化'] },
|
||||||
|
{ lv3_id: 'L3_PV', lv3_name: '光伏', lv2_id: 'L2_NEW_ENERGY', lv2_name: '新能源 (光伏/储能/电池)', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['光伏', '太阳能', '硅片', '组件'] },
|
||||||
|
{ lv3_id: 'L3_STORAGE', lv3_name: '储能与电池', lv2_id: 'L2_NEW_ENERGY', lv2_name: '新能源 (光伏/储能/电池)', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['储能', '电池', '锂电', '固态电池', '新能源'] },
|
||||||
|
{ lv3_id: 'L3_EV_OEM', lv3_name: '新能源整车', lv2_id: 'L2_EV', lv2_name: '智能网联汽车', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['新能源汽车', '电动车', '比亚迪', '特斯拉', '整车'] },
|
||||||
|
{ lv3_id: 'L3_AUTO_DRIVE', lv3_name: '智能驾驶', lv2_id: 'L2_EV', lv2_name: '智能网联汽车', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['智能驾驶', '自动驾驶', '智能网联', '车路协同'] },
|
||||||
|
{ lv3_id: 'L3_DRONE', lv3_name: '无人机', lv2_id: 'L2_LOW_ALTITUDE', lv2_name: '低空经济 (无人机/eVTOL)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['无人机', '低空', '空域'] },
|
||||||
|
{ lv3_id: 'L3_EVTOL', lv3_name: 'eVTOL', lv2_id: 'L2_LOW_ALTITUDE', lv2_name: '低空经济 (无人机/eVTOL)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['eVTOL', '飞行汽车', '空中出租车'] },
|
||||||
|
{ lv3_id: 'L3_AEROSPACE', lv3_name: '航空航天', lv2_id: 'L2_MILITARY', lv2_name: '军工 (航空航天/国防)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['航空', '航天', '卫星', '火箭', '军工'] },
|
||||||
|
{ lv3_id: 'L3_DEFENSE', lv3_name: '国防军工', lv2_id: 'L2_MILITARY', lv2_name: '军工 (航空航天/国防)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['国防', '导弹', '军工装备'] },
|
||||||
|
{ lv3_id: 'L3_DRUG', lv3_name: '创新药', lv2_id: 'L2_PHARMA', lv2_name: '医药医疗 (创新药/器械)', lv1_id: 'L1_PHARMA', lv1_name: '医药健康', keywords: ['创新药', '医药', '生物', 'CXO'] },
|
||||||
|
{ lv3_id: 'L3_DEVICE', lv3_name: '医疗器械', lv2_id: 'L2_PHARMA', lv2_name: '医药医疗 (创新药/器械)', lv1_id: 'L1_PHARMA', lv1_name: '医药健康', keywords: ['医疗器械', '医疗', '器械'] },
|
||||||
|
{ lv3_id: 'L3_BANK', lv3_name: '银行', lv2_id: 'L2_FINANCE', lv2_name: '金融 (银行/券商/保险)', lv1_id: 'L1_FINANCE', lv1_name: '金融', keywords: ['银行', '金融'] },
|
||||||
|
{ lv3_id: 'L3_BROKER', lv3_name: '券商', lv2_id: 'L2_FINANCE', lv2_name: '金融 (银行/券商/保险)', lv1_id: 'L1_FINANCE', lv1_name: '金融', keywords: ['券商', '证券'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hierarchyOptions = {
|
||||||
|
lv1: [...new Map(mainlineDefinitions.map(m => [m.lv1_id, { id: m.lv1_id, name: m.lv1_name }])).values()],
|
||||||
|
lv2: [...new Map(mainlineDefinitions.map(m => [m.lv2_id, { id: m.lv2_id, name: m.lv2_name, lv1_id: m.lv1_id, lv1_name: m.lv1_name }])).values()],
|
||||||
|
lv3: mainlineDefinitions.map(m => ({ id: m.lv3_id, name: m.lv3_name, lv2_id: m.lv2_id, lv2_name: m.lv2_name, lv1_id: m.lv1_id, lv1_name: m.lv1_name })),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mainlineGroups = {};
|
||||||
|
const isSpecificId = groupBy.startsWith('L1_') || groupBy.startsWith('L2_') || groupBy.startsWith('L3_');
|
||||||
|
|
||||||
|
allEvents.forEach(event => {
|
||||||
|
const keywords = event.keywords || event.related_concepts || [];
|
||||||
|
const conceptNames = keywords.map(k => {
|
||||||
|
if (typeof k === 'string') return k;
|
||||||
|
if (typeof k === 'object' && k !== null) return k.concept || k.name || '';
|
||||||
|
return '';
|
||||||
|
}).filter(Boolean).join(' ');
|
||||||
|
const titleAndDesc = `${event.title || ''} ${event.description || ''}`;
|
||||||
|
const textToMatch = `${conceptNames} ${titleAndDesc}`.toLowerCase();
|
||||||
|
|
||||||
|
mainlineDefinitions.forEach(mainline => {
|
||||||
|
const matched = mainline.keywords.some(kw => textToMatch.includes(kw.toLowerCase()));
|
||||||
|
if (matched) {
|
||||||
|
let groupKey, groupData;
|
||||||
|
|
||||||
|
if (isSpecificId) {
|
||||||
|
if (groupBy.startsWith('L1_') && mainline.lv1_id === groupBy) {
|
||||||
|
groupKey = mainline.lv2_id;
|
||||||
|
groupData = { group_id: mainline.lv2_id, group_name: mainline.lv2_name, parent_name: mainline.lv1_name, events: [] };
|
||||||
|
} else if (groupBy.startsWith('L2_') && mainline.lv2_id === groupBy) {
|
||||||
|
groupKey = mainline.lv3_id;
|
||||||
|
groupData = { group_id: mainline.lv3_id, group_name: mainline.lv3_name, parent_name: mainline.lv2_name, grandparent_name: mainline.lv1_name, events: [] };
|
||||||
|
} else if (groupBy.startsWith('L3_') && mainline.lv3_id === groupBy) {
|
||||||
|
groupKey = mainline.lv3_id;
|
||||||
|
groupData = { group_id: mainline.lv3_id, group_name: mainline.lv3_name, parent_name: mainline.lv2_name, grandparent_name: mainline.lv1_name, events: [] };
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (groupBy === 'lv1') {
|
||||||
|
groupKey = mainline.lv1_id;
|
||||||
|
groupData = { group_id: mainline.lv1_id, group_name: mainline.lv1_name, events: [] };
|
||||||
|
} else if (groupBy === 'lv3') {
|
||||||
|
groupKey = mainline.lv3_id;
|
||||||
|
groupData = { group_id: mainline.lv3_id, group_name: mainline.lv3_name, parent_name: mainline.lv2_name, grandparent_name: mainline.lv1_name, events: [] };
|
||||||
|
} else {
|
||||||
|
groupKey = mainline.lv2_id;
|
||||||
|
groupData = { group_id: mainline.lv2_id, group_name: mainline.lv2_name, parent_name: mainline.lv1_name, events: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mainlineGroups[groupKey]) {
|
||||||
|
mainlineGroups[groupKey] = groupData;
|
||||||
|
}
|
||||||
|
if (!mainlineGroups[groupKey].events.find(e => e.id === event.id)) {
|
||||||
|
mainlineGroups[groupKey].events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const generatePriceData = () => parseFloat((Math.random() * 13 - 5).toFixed(2));
|
||||||
|
const priceDataMap = { lv1: {}, lv2: {}, lv3: {} };
|
||||||
|
|
||||||
|
mainlineDefinitions.forEach(def => {
|
||||||
|
if (!priceDataMap.lv1[def.lv1_name]) priceDataMap.lv1[def.lv1_name] = generatePriceData();
|
||||||
|
if (!priceDataMap.lv2[def.lv2_name]) priceDataMap.lv2[def.lv2_name] = generatePriceData();
|
||||||
|
if (!priceDataMap.lv3[def.lv3_name]) priceDataMap.lv3[def.lv3_name] = generatePriceData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainlines = Object.values(mainlineGroups)
|
||||||
|
.map(group => {
|
||||||
|
let avgChangePct = null;
|
||||||
|
if (groupBy === 'lv1' || groupBy.startsWith('L1_')) {
|
||||||
|
avgChangePct = groupBy.startsWith('L1_') ? priceDataMap.lv2[group.group_name] : priceDataMap.lv1[group.group_name];
|
||||||
|
} else if (groupBy === 'lv3' || groupBy.startsWith('L2_')) {
|
||||||
|
avgChangePct = priceDataMap.lv3[group.group_name];
|
||||||
|
} else {
|
||||||
|
avgChangePct = priceDataMap.lv2[group.group_name];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
events: group.events.slice(0, limitPerMainline),
|
||||||
|
event_count: Math.min(group.events.length, limitPerMainline),
|
||||||
|
avg_change_pct: avgChangePct ?? null,
|
||||||
|
price_date: new Date().toISOString().split('T')[0]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(group => group.event_count > 0)
|
||||||
|
.sort((a, b) => b.event_count - a.event_count);
|
||||||
|
|
||||||
|
const groupedEventIds = new Set();
|
||||||
|
mainlines.forEach(m => m.events.forEach(e => groupedEventIds.add(e.id)));
|
||||||
|
const ungroupedCount = allEvents.filter(e => !groupedEventIds.has(e.id)).length;
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
mainlines,
|
||||||
|
total_events: allEvents.length,
|
||||||
|
mainline_count: mainlines.length,
|
||||||
|
ungrouped_count: ungroupedCount,
|
||||||
|
group_by: groupBy,
|
||||||
|
hierarchy_options: hierarchyOptions,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mock Event] 主线数据获取失败:', error);
|
||||||
|
return HttpResponse.json({ success: false, error: error.message || '获取主线数据失败' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
// ==================== 事件详情相关 ====================
|
// ==================== 事件详情相关 ====================
|
||||||
|
|
||||||
// 获取事件详情
|
// 获取事件详情
|
||||||
@@ -1585,234 +1738,4 @@ export const eventHandlers = [
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// ==================== 主线模式相关 ====================
|
|
||||||
|
|
||||||
// 获取按主线(lv1/lv2/lv3概念)分组的事件列表
|
|
||||||
http.get('/api/events/mainline', async ({ request }) => {
|
|
||||||
await delay(500);
|
|
||||||
|
|
||||||
const url = new URL(request.url);
|
|
||||||
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' | 'lv3' | 具体的概念ID(如 'L1_TMT', 'L2_AI_INFRA', 'L3_AI_CHIP')
|
|
||||||
const groupBy = url.searchParams.get('group_by') || 'lv2';
|
|
||||||
|
|
||||||
console.log('[Mock Event] 获取主线数据:', { recentDays, importance, limitPerMainline, groupBy });
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 生成 mock 事件数据
|
|
||||||
const allEvents = generateDynamicNewsEvents(100);
|
|
||||||
|
|
||||||
// 定义完整的 lv1 -> lv2 -> lv3 层级结构
|
|
||||||
const mainlineDefinitions = [
|
|
||||||
{ lv3_id: 'L3_AI_CHIP', lv3_name: 'AI芯片与算力', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['算力', 'AI芯片', 'GPU', '英伟达', '华为昇腾', '寒武纪'] },
|
|
||||||
{ lv3_id: 'L3_AI_SERVER', lv3_name: '服务器与数据中心', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['服务器', '数据中心', '智算中心', '液冷'] },
|
|
||||||
{ lv3_id: 'L3_OPTICAL', lv3_name: '光通信与CPO', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['CPO', '光通信', '光模块', '光芯片'] },
|
|
||||||
{ lv3_id: 'L3_PCB', lv3_name: 'PCB与封装', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['PCB', '封装', 'AI PCB'] },
|
|
||||||
{ lv3_id: 'L3_AI_APP', lv3_name: 'AI应用与大模型', lv2_id: 'L2_AI_INFRA', lv2_name: 'AI基础设施 (算力/CPO/PCB)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['大模型', '智能体', 'AI', '人工智能', 'DeepSeek', 'KIMI', 'ChatGPT'] },
|
|
||||||
{ lv3_id: 'L3_CHIP_DESIGN', lv3_name: '芯片设计', lv2_id: 'L2_SEMICONDUCTOR', lv2_name: '半导体 (设计/制造/封测)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['芯片设计', '半导体', 'IC设计'] },
|
|
||||||
{ lv3_id: 'L3_CHIP_MFG', lv3_name: '芯片制造', lv2_id: 'L2_SEMICONDUCTOR', lv2_name: '半导体 (设计/制造/封测)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['晶圆', '光刻', '芯片制造', '中芯国际'] },
|
|
||||||
{ lv3_id: 'L3_HUMANOID', lv3_name: '人形机器人', lv2_id: 'L2_ROBOT', lv2_name: '机器人 (人形机器人/工业机器人)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['人形机器人', '具身智能', '特斯拉机器人'] },
|
|
||||||
{ lv3_id: 'L3_INDUSTRIAL_ROBOT', lv3_name: '工业机器人', lv2_id: 'L2_ROBOT', lv2_name: '机器人 (人形机器人/工业机器人)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['工业机器人', '自动化', '机器人'] },
|
|
||||||
{ lv3_id: 'L3_MOBILE', lv3_name: '智能手机', lv2_id: 'L2_CONSUMER_ELEC', lv2_name: '消费电子 (手机/XR/可穿戴)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['手机', '华为', '苹果', '小米', '折叠屏'] },
|
|
||||||
{ lv3_id: 'L3_XR', lv3_name: 'XR与可穿戴', lv2_id: 'L2_CONSUMER_ELEC', lv2_name: '消费电子 (手机/XR/可穿戴)', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['XR', 'VR', 'AR', '可穿戴', 'MR', 'Vision Pro'] },
|
|
||||||
{ lv3_id: 'L3_5G', lv3_name: '5G/6G通信', lv2_id: 'L2_TELECOM', lv2_name: '通信、互联网与软件', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['5G', '6G', '通信', '基站'] },
|
|
||||||
{ lv3_id: 'L3_CLOUD', lv3_name: '云计算与软件', lv2_id: 'L2_TELECOM', lv2_name: '通信、互联网与软件', lv1_id: 'L1_TMT', lv1_name: 'TMT (科技/媒体/通信)', keywords: ['云计算', '软件', 'SaaS', '互联网', '数字化'] },
|
|
||||||
{ lv3_id: 'L3_PV', lv3_name: '光伏', lv2_id: 'L2_NEW_ENERGY', lv2_name: '新能源 (光伏/储能/电池)', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['光伏', '太阳能', '硅片', '组件'] },
|
|
||||||
{ lv3_id: 'L3_STORAGE', lv3_name: '储能与电池', lv2_id: 'L2_NEW_ENERGY', lv2_name: '新能源 (光伏/储能/电池)', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['储能', '电池', '锂电', '固态电池', '新能源'] },
|
|
||||||
{ lv3_id: 'L3_EV_OEM', lv3_name: '新能源整车', lv2_id: 'L2_EV', lv2_name: '智能网联汽车', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['新能源汽车', '电动车', '比亚迪', '特斯拉', '整车'] },
|
|
||||||
{ lv3_id: 'L3_AUTO_DRIVE', lv3_name: '智能驾驶', lv2_id: 'L2_EV', lv2_name: '智能网联汽车', lv1_id: 'L1_NEW_ENERGY_ENV', lv1_name: '新能源与智能汽车', keywords: ['智能驾驶', '自动驾驶', '智能网联', '车路协同'] },
|
|
||||||
{ lv3_id: 'L3_DRONE', lv3_name: '无人机', lv2_id: 'L2_LOW_ALTITUDE', lv2_name: '低空经济 (无人机/eVTOL)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['无人机', '低空', '空域'] },
|
|
||||||
{ lv3_id: 'L3_EVTOL', lv3_name: 'eVTOL', lv2_id: 'L2_LOW_ALTITUDE', lv2_name: '低空经济 (无人机/eVTOL)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['eVTOL', '飞行汽车', '空中出租车'] },
|
|
||||||
{ lv3_id: 'L3_AEROSPACE', lv3_name: '航空航天', lv2_id: 'L2_MILITARY', lv2_name: '军工 (航空航天/国防)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['航空', '航天', '卫星', '火箭', '军工'] },
|
|
||||||
{ lv3_id: 'L3_DEFENSE', lv3_name: '国防军工', lv2_id: 'L2_MILITARY', lv2_name: '军工 (航空航天/国防)', lv1_id: 'L1_ADVANCED_MFG', lv1_name: '先进制造', keywords: ['国防', '导弹', '军工装备'] },
|
|
||||||
{ lv3_id: 'L3_DRUG', lv3_name: '创新药', lv2_id: 'L2_PHARMA', lv2_name: '医药医疗 (创新药/器械)', lv1_id: 'L1_PHARMA', lv1_name: '医药健康', keywords: ['创新药', '医药', '生物', 'CXO'] },
|
|
||||||
{ lv3_id: 'L3_DEVICE', lv3_name: '医疗器械', lv2_id: 'L2_PHARMA', lv2_name: '医药医疗 (创新药/器械)', lv1_id: 'L1_PHARMA', lv1_name: '医药健康', keywords: ['医疗器械', '医疗', '器械'] },
|
|
||||||
{ lv3_id: 'L3_BANK', lv3_name: '银行', lv2_id: 'L2_FINANCE', lv2_name: '金融 (银行/券商/保险)', lv1_id: 'L1_FINANCE', lv1_name: '金融', keywords: ['银行', '金融'] },
|
|
||||||
{ lv3_id: 'L3_BROKER', lv3_name: '券商', lv2_id: 'L2_FINANCE', lv2_name: '金融 (银行/券商/保险)', lv1_id: 'L1_FINANCE', lv1_name: '金融', keywords: ['券商', '证券'] },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 生成层级选项供前端下拉框使用
|
|
||||||
const hierarchyOptions = {
|
|
||||||
lv1: [...new Map(mainlineDefinitions.map(m => [m.lv1_id, { id: m.lv1_id, name: m.lv1_name }])).values()],
|
|
||||||
lv2: [...new Map(mainlineDefinitions.map(m => [m.lv2_id, { id: m.lv2_id, name: m.lv2_name, lv1_id: m.lv1_id, lv1_name: m.lv1_name }])).values()],
|
|
||||||
lv3: mainlineDefinitions.map(m => ({ id: m.lv3_id, name: m.lv3_name, lv2_id: m.lv2_id, lv2_name: m.lv2_name, lv1_id: m.lv1_id, lv1_name: m.lv1_name })),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 按主线分组事件
|
|
||||||
const mainlineGroups = {};
|
|
||||||
|
|
||||||
// 判断分组方式
|
|
||||||
const isSpecificId = groupBy.startsWith('L1_') || groupBy.startsWith('L2_') || groupBy.startsWith('L3_');
|
|
||||||
|
|
||||||
allEvents.forEach(event => {
|
|
||||||
const keywords = event.keywords || event.related_concepts || [];
|
|
||||||
const conceptNames = keywords.map(k => k.concept || k.name || k).join(' ');
|
|
||||||
const titleAndDesc = `${event.title || ''} ${event.description || ''}`;
|
|
||||||
const textToMatch = `${conceptNames} ${titleAndDesc}`.toLowerCase();
|
|
||||||
|
|
||||||
mainlineDefinitions.forEach(mainline => {
|
|
||||||
const matched = mainline.keywords.some(kw => textToMatch.includes(kw.toLowerCase()));
|
|
||||||
if (matched) {
|
|
||||||
let groupKey, groupData;
|
|
||||||
|
|
||||||
if (isSpecificId) {
|
|
||||||
// 筛选特定概念ID
|
|
||||||
if (groupBy.startsWith('L1_') && mainline.lv1_id === groupBy) {
|
|
||||||
groupKey = mainline.lv2_id;
|
|
||||||
groupData = {
|
|
||||||
group_id: mainline.lv2_id,
|
|
||||||
group_name: mainline.lv2_name,
|
|
||||||
parent_name: mainline.lv1_name,
|
|
||||||
events: []
|
|
||||||
};
|
|
||||||
} else if (groupBy.startsWith('L2_') && mainline.lv2_id === groupBy) {
|
|
||||||
groupKey = mainline.lv3_id;
|
|
||||||
groupData = {
|
|
||||||
group_id: mainline.lv3_id,
|
|
||||||
group_name: mainline.lv3_name,
|
|
||||||
parent_name: mainline.lv2_name,
|
|
||||||
grandparent_name: mainline.lv1_name,
|
|
||||||
events: []
|
|
||||||
};
|
|
||||||
} else if (groupBy.startsWith('L3_') && mainline.lv3_id === groupBy) {
|
|
||||||
groupKey = mainline.lv3_id;
|
|
||||||
groupData = {
|
|
||||||
group_id: mainline.lv3_id,
|
|
||||||
group_name: mainline.lv3_name,
|
|
||||||
parent_name: mainline.lv2_name,
|
|
||||||
grandparent_name: mainline.lv1_name,
|
|
||||||
events: []
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (groupBy === 'lv1') {
|
|
||||||
groupKey = mainline.lv1_id;
|
|
||||||
groupData = {
|
|
||||||
group_id: mainline.lv1_id,
|
|
||||||
group_name: mainline.lv1_name,
|
|
||||||
events: []
|
|
||||||
};
|
|
||||||
} else if (groupBy === 'lv3') {
|
|
||||||
groupKey = mainline.lv3_id;
|
|
||||||
groupData = {
|
|
||||||
group_id: mainline.lv3_id,
|
|
||||||
group_name: mainline.lv3_name,
|
|
||||||
parent_name: mainline.lv2_name,
|
|
||||||
grandparent_name: mainline.lv1_name,
|
|
||||||
events: []
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 默认 lv2
|
|
||||||
groupKey = mainline.lv2_id;
|
|
||||||
groupData = {
|
|
||||||
group_id: mainline.lv2_id,
|
|
||||||
group_name: mainline.lv2_name,
|
|
||||||
parent_name: mainline.lv1_name,
|
|
||||||
events: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mainlineGroups[groupKey]) {
|
|
||||||
mainlineGroups[groupKey] = groupData;
|
|
||||||
}
|
|
||||||
if (!mainlineGroups[groupKey].events.find(e => e.id === event.id)) {
|
|
||||||
mainlineGroups[groupKey].events.push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成各层级的模拟涨跌幅数据
|
|
||||||
const generatePriceData = () => {
|
|
||||||
// 生成 -5% 到 +8% 之间的随机涨跌幅
|
|
||||||
return parseFloat((Math.random() * 13 - 5).toFixed(2));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 为各层级生成涨跌幅 Map
|
|
||||||
const priceDataMap = {
|
|
||||||
lv1: {},
|
|
||||||
lv2: {},
|
|
||||||
lv3: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 为每个层级生成涨跌幅
|
|
||||||
mainlineDefinitions.forEach(def => {
|
|
||||||
if (!priceDataMap.lv1[def.lv1_name]) {
|
|
||||||
priceDataMap.lv1[def.lv1_name] = generatePriceData();
|
|
||||||
}
|
|
||||||
if (!priceDataMap.lv2[def.lv2_name]) {
|
|
||||||
priceDataMap.lv2[def.lv2_name] = generatePriceData();
|
|
||||||
}
|
|
||||||
if (!priceDataMap.lv3[def.lv3_name]) {
|
|
||||||
priceDataMap.lv3[def.lv3_name] = generatePriceData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mainlines = Object.values(mainlineGroups)
|
|
||||||
.map(group => {
|
|
||||||
// 根据分组层级获取对应的涨跌幅
|
|
||||||
let avgChangePct = null;
|
|
||||||
if (groupBy === 'lv1' || groupBy.startsWith('L1_')) {
|
|
||||||
// lv1 分组,使用 lv1 涨跌幅(如果是 L1_ 开头,则显示的是 lv2 分组,使用 lv2 涨跌幅)
|
|
||||||
if (groupBy.startsWith('L1_')) {
|
|
||||||
avgChangePct = priceDataMap.lv2[group.group_name] ?? null;
|
|
||||||
} else {
|
|
||||||
avgChangePct = priceDataMap.lv1[group.group_name] ?? null;
|
|
||||||
}
|
|
||||||
} else if (groupBy === 'lv3' || groupBy.startsWith('L2_')) {
|
|
||||||
// lv3 分组,使用 lv3 涨跌幅
|
|
||||||
avgChangePct = priceDataMap.lv3[group.group_name] ?? null;
|
|
||||||
} else {
|
|
||||||
// lv2 分组(默认),使用 lv2 涨跌幅
|
|
||||||
avgChangePct = priceDataMap.lv2[group.group_name] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...group,
|
|
||||||
events: group.events.slice(0, limitPerMainline),
|
|
||||||
event_count: Math.min(group.events.length, limitPerMainline),
|
|
||||||
avg_change_pct: avgChangePct,
|
|
||||||
price_date: new Date().toISOString().split('T')[0]
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(group => group.event_count > 0)
|
|
||||||
.sort((a, b) => b.event_count - a.event_count);
|
|
||||||
|
|
||||||
const groupedEventIds = new Set();
|
|
||||||
mainlines.forEach(m => m.events.forEach(e => groupedEventIds.add(e.id)));
|
|
||||||
const ungroupedCount = allEvents.filter(e => !groupedEventIds.has(e.id)).length;
|
|
||||||
|
|
||||||
console.log('[Mock Event] 主线数据生成完成:', {
|
|
||||||
mainlineCount: mainlines.length,
|
|
||||||
totalEvents: allEvents.length,
|
|
||||||
ungroupedCount,
|
|
||||||
groupBy
|
|
||||||
});
|
|
||||||
|
|
||||||
return HttpResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
mainlines,
|
|
||||||
total_events: allEvents.length,
|
|
||||||
mainline_count: mainlines.length,
|
|
||||||
ungrouped_count: ungroupedCount,
|
|
||||||
group_by: groupBy,
|
|
||||||
hierarchy_options: hierarchyOptions,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Mock Event] 主线数据获取失败:', error);
|
|
||||||
return HttpResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error.message || '获取主线数据失败'
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -630,7 +630,6 @@ const MainlineTimelineViewComponent = forwardRef(
|
|||||||
params.append("group_by", groupBy);
|
params.append("group_by", groupBy);
|
||||||
|
|
||||||
const url = `${apiBase}/api/events/mainline?${params.toString()}`;
|
const url = `${apiBase}/api/events/mainline?${params.toString()}`;
|
||||||
console.log("[MainlineTimelineView] 🔄 请求主线数据:", url);
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -638,25 +637,22 @@ const MainlineTimelineViewComponent = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("[MainlineTimelineView] 📦 响应数据:", {
|
|
||||||
success: result.success,
|
// 兼容两种响应格式:{ success, data: {...} } 或 { success, mainlines, ... }
|
||||||
mainlineCount: result.data?.mainlines?.length,
|
const responseData = result.data || result;
|
||||||
totalEvents: result.data?.total_events,
|
|
||||||
groupBy: result.data?.group_by,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 保存原始数据,排序在渲染时根据 sortBy 状态进行
|
// 保存原始数据,排序在渲染时根据 sortBy 状态进行
|
||||||
setMainlineData(result.data);
|
setMainlineData(responseData);
|
||||||
|
|
||||||
// 保存层级选项供下拉框使用
|
// 保存层级选项供下拉框使用
|
||||||
if (result.data.hierarchy_options) {
|
if (responseData.hierarchy_options) {
|
||||||
setHierarchyOptions(result.data.hierarchy_options);
|
setHierarchyOptions(responseData.hierarchy_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化展开状态(默认全部展开)
|
// 初始化展开状态(默认全部展开)
|
||||||
const initialExpanded = {};
|
const initialExpanded = {};
|
||||||
(result.data.mainlines || []).forEach((mainline) => {
|
(responseData.mainlines || []).forEach((mainline) => {
|
||||||
const groupId = mainline.group_id || mainline.lv2_id || mainline.lv1_id;
|
const groupId = mainline.group_id || mainline.lv2_id || mainline.lv1_id;
|
||||||
initialExpanded[groupId] = true;
|
initialExpanded[groupId] = true;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user