更新Company页面的UI为FUI风格
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -47,15 +47,28 @@ const fetchConceptHierarchy = async () => {
|
|||||||
conceptHierarchyPromise = (async () => {
|
conceptHierarchyPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const apiBase = getApiBase();
|
const apiBase = getApiBase();
|
||||||
const response = await fetch(`${apiBase}/concept-api/hierarchy`);
|
const url = `${apiBase}/concept-api/hierarchy`;
|
||||||
|
console.log('[GroupedFourRowGrid] 🔄 正在请求概念层级:', url);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('[GroupedFourRowGrid] 📦 API 响应数据:', {
|
||||||
|
hasHierarchy: !!data.hierarchy,
|
||||||
|
hierarchyLength: data.hierarchy?.length,
|
||||||
|
keys: Object.keys(data),
|
||||||
|
sample: data.hierarchy?.[0] // 打印第一个 lv1 作为样本
|
||||||
|
});
|
||||||
|
|
||||||
// 构建概念名称 -> lv2 映射
|
// 构建概念名称 -> lv2 映射
|
||||||
const mapping = {};
|
const mapping = {};
|
||||||
const hierarchy = data.hierarchy || [];
|
const hierarchy = data.hierarchy || data.data?.hierarchy || data || [];
|
||||||
|
|
||||||
|
// 如果 hierarchy 不是数组,尝试其他格式
|
||||||
|
const hierarchyArray = Array.isArray(hierarchy) ? hierarchy : [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归添加概念到映射
|
* 递归添加概念到映射
|
||||||
@@ -74,7 +87,7 @@ const fetchConceptHierarchy = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
hierarchy.forEach(lv1 => {
|
hierarchyArray.forEach(lv1 => {
|
||||||
const lv1Name = lv1.name;
|
const lv1Name = lv1.name;
|
||||||
const lv2List = lv1.children || [];
|
const lv2List = lv1.children || [];
|
||||||
|
|
||||||
@@ -97,11 +110,13 @@ const fetchConceptHierarchy = async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[GroupedFourRowGrid] 概念层级映射加载完成,共', Object.keys(mapping).length, '个概念');
|
console.log('[GroupedFourRowGrid] ✅ 概念层级映射加载完成,共', Object.keys(mapping).length, '个概念');
|
||||||
|
console.log('[GroupedFourRowGrid] 📋 映射样本:', Object.entries(mapping).slice(0, 10));
|
||||||
|
|
||||||
conceptHierarchyCache = mapping;
|
conceptHierarchyCache = mapping;
|
||||||
return mapping;
|
return mapping;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GroupedFourRowGrid] 获取概念层级失败:', error);
|
console.error('[GroupedFourRowGrid] ❌ 获取概念层级失败:', error);
|
||||||
conceptHierarchyPromise = null; // 允许重试
|
conceptHierarchyPromise = null; // 允许重试
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -130,23 +145,50 @@ const useConceptHierarchy = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从事件的 keywords (related_concepts) 中提取主要的 lv2 分类
|
* 在映射表中查找概念(支持精确匹配和模糊匹配)
|
||||||
|
* @param {string} conceptName - 概念名称
|
||||||
|
* @param {Object} hierarchyMap - 概念层级映射
|
||||||
|
* @returns {Object|null} 匹配到的层级信息
|
||||||
|
*/
|
||||||
|
const findConceptInMap = (conceptName, hierarchyMap) => {
|
||||||
|
if (!conceptName || !hierarchyMap) return null;
|
||||||
|
|
||||||
|
// 1. 精确匹配
|
||||||
|
if (hierarchyMap[conceptName]) {
|
||||||
|
return hierarchyMap[conceptName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 模糊匹配:遍历映射表,查找包含关系
|
||||||
|
const conceptKeys = Object.keys(hierarchyMap);
|
||||||
|
for (const key of conceptKeys) {
|
||||||
|
// 概念名包含映射表中的关键词,或映射表中的关键词包含概念名
|
||||||
|
if (conceptName.includes(key) || key.includes(conceptName)) {
|
||||||
|
return hierarchyMap[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从事件的 keywords (related_concepts) 中提取所有相关的 lv2 分类
|
||||||
* @param {Object} event - 事件对象
|
* @param {Object} event - 事件对象
|
||||||
* @param {Object} hierarchyMap - 概念层级映射 { 概念名: { lv1, lv2, lv3 } }
|
* @param {Object} hierarchyMap - 概念层级映射 { 概念名: { lv1, lv2, lv3 } }
|
||||||
* @returns {string} lv2 分类名称
|
* @returns {Array<string>} lv2 分类名称数组(去重)
|
||||||
*/
|
*/
|
||||||
const getEventMainLine = (event, hierarchyMap = {}) => {
|
const getEventLv2List = (event, hierarchyMap = {}) => {
|
||||||
// keywords 即 related_concepts
|
// keywords 即 related_concepts
|
||||||
// 真实数据结构: [{ concept: '煤炭', reason: '...' }, ...]
|
// 真实数据结构: [{ concept: '煤炭', reason: '...' }, ...]
|
||||||
// Mock 数据可能有 hierarchy 字段
|
// Mock 数据可能有 hierarchy 字段
|
||||||
const keywords = event.keywords || event.related_concepts || [];
|
const keywords = event.keywords || event.related_concepts || [];
|
||||||
|
|
||||||
if (!keywords.length) {
|
if (!keywords.length) {
|
||||||
return '其他';
|
return ['其他'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计各 lv2 出现次数,取出现最多的
|
const lv2Set = new Set();
|
||||||
const lv2Counts = {};
|
const unmatchedConcepts = []; // 记录未匹配的概念
|
||||||
|
|
||||||
keywords.forEach(keyword => {
|
keywords.forEach(keyword => {
|
||||||
// 优先使用 keyword 自带的 hierarchy(Mock 数据)
|
// 优先使用 keyword 自带的 hierarchy(Mock 数据)
|
||||||
let lv2 = keyword.hierarchy?.lv2;
|
let lv2 = keyword.hierarchy?.lv2;
|
||||||
@@ -154,30 +196,36 @@ const getEventMainLine = (event, hierarchyMap = {}) => {
|
|||||||
// 如果没有,从映射表查找(真实数据)
|
// 如果没有,从映射表查找(真实数据)
|
||||||
if (!lv2) {
|
if (!lv2) {
|
||||||
const conceptName = keyword.concept || keyword.name || keyword;
|
const conceptName = keyword.concept || keyword.name || keyword;
|
||||||
const hierarchy = hierarchyMap[conceptName];
|
// 使用模糊匹配
|
||||||
|
const hierarchy = findConceptInMap(conceptName, hierarchyMap);
|
||||||
lv2 = hierarchy?.lv2;
|
lv2 = hierarchy?.lv2;
|
||||||
|
|
||||||
|
// 记录未匹配的概念(用于调试)
|
||||||
|
if (!lv2 && conceptName) {
|
||||||
|
unmatchedConcepts.push(conceptName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lv2) {
|
if (lv2) {
|
||||||
lv2Counts[lv2] = (lv2Counts[lv2] || 0) + 1;
|
lv2Set.add(lv2);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 找出出现次数最多的 lv2
|
// 调试:输出未匹配的概念(只在有未匹配时输出)
|
||||||
let maxCount = 0;
|
if (unmatchedConcepts.length > 0 && lv2Set.size === 0) {
|
||||||
let mainLv2 = '其他';
|
console.log(`[GroupedFourRowGrid] 事件 "${event.title?.substring(0, 30)}..." 全部 ${unmatchedConcepts.length} 个概念未匹配:`, unmatchedConcepts);
|
||||||
Object.entries(lv2Counts).forEach(([lv2, count]) => {
|
}
|
||||||
if (count > maxCount) {
|
|
||||||
maxCount = count;
|
|
||||||
mainLv2 = lv2;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return mainLv2;
|
// 如果没有匹配到任何 lv2,返回"其他"
|
||||||
|
if (lv2Set.size === 0) {
|
||||||
|
return ['其他'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(lv2Set);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 lv2 分组事件
|
* 按 lv2 分组事件(一个事件可以属于多个 lv2 分组)
|
||||||
* @param {Array} events - 事件列表
|
* @param {Array} events - 事件列表
|
||||||
* @param {Object} hierarchyMap - 概念层级映射
|
* @param {Object} hierarchyMap - 概念层级映射
|
||||||
* @returns {Array} 分组后的数组 [{ lv2, events: [] }, ...]
|
* @returns {Array} 分组后的数组 [{ lv2, events: [] }, ...]
|
||||||
@@ -185,22 +233,35 @@ const getEventMainLine = (event, hierarchyMap = {}) => {
|
|||||||
const groupEventsByLv2 = (events, hierarchyMap = {}) => {
|
const groupEventsByLv2 = (events, hierarchyMap = {}) => {
|
||||||
const groups = {};
|
const groups = {};
|
||||||
|
|
||||||
|
// 调试:检查 hierarchyMap 是否有数据
|
||||||
|
const mapSize = Object.keys(hierarchyMap).length;
|
||||||
|
console.log(`[GroupedFourRowGrid] 概念映射表大小: ${mapSize}`, mapSize > 0 ? '✅' : '❌ 映射表为空!');
|
||||||
|
|
||||||
events.forEach(event => {
|
events.forEach(event => {
|
||||||
const lv2 = getEventMainLine(event, hierarchyMap);
|
// 获取该事件对应的所有 lv2 分类
|
||||||
if (!groups[lv2]) {
|
const lv2List = getEventLv2List(event, hierarchyMap);
|
||||||
groups[lv2] = [];
|
|
||||||
}
|
// 将事件添加到每个相关的 lv2 分组中
|
||||||
groups[lv2].push(event);
|
lv2List.forEach(lv2 => {
|
||||||
|
if (!groups[lv2]) {
|
||||||
|
groups[lv2] = [];
|
||||||
|
}
|
||||||
|
groups[lv2].push(event);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 转换为数组并按事件数量排序(多的在前)
|
// 转换为数组并按事件数量排序(多的在前)
|
||||||
return Object.entries(groups)
|
const result = Object.entries(groups)
|
||||||
.map(([lv2, groupEvents]) => ({
|
.map(([lv2, groupEvents]) => ({
|
||||||
lv2,
|
lv2,
|
||||||
events: groupEvents,
|
events: groupEvents,
|
||||||
eventCount: groupEvents.length,
|
eventCount: groupEvents.length,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.eventCount - a.eventCount);
|
.sort((a, b) => b.eventCount - a.eventCount);
|
||||||
|
|
||||||
|
console.log(`[GroupedFourRowGrid] 分组结果:`, result.map(g => `${g.lv2}(${g.eventCount})`).join(', '));
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -212,67 +273,95 @@ const GroupHeader = ({ lv2, eventCount, isExpanded, onToggle, colorScheme }) =>
|
|||||||
const textColor = useColorModeValue('gray.700', 'gray.200');
|
const textColor = useColorModeValue('gray.700', 'gray.200');
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
|
||||||
// 根据主线类型获取配色
|
// 根据主线类型获取配色(使用关键词匹配)
|
||||||
const getColorScheme = (lv2Name) => {
|
const getColorScheme = (lv2Name) => {
|
||||||
const colorMap = {
|
if (!lv2Name) return 'gray';
|
||||||
// 人工智能相关
|
|
||||||
'AI基础设施': 'purple',
|
const name = lv2Name.toLowerCase();
|
||||||
'AI模型与软件': 'purple',
|
|
||||||
'AI应用': 'purple',
|
// AI / 人工智能相关 - 紫色系
|
||||||
// 半导体相关
|
if (name.includes('ai') || name.includes('人工智能') || name.includes('算力') ||
|
||||||
'半导体设备': 'blue',
|
name.includes('大模型') || name.includes('智能体')) {
|
||||||
'半导体材料': 'blue',
|
return 'purple';
|
||||||
'芯片设计与制造': 'blue',
|
}
|
||||||
'先进封装': 'blue',
|
|
||||||
// 机器人相关
|
// 半导体 / 芯片相关 - 蓝色系
|
||||||
'人形机器人整机': 'pink',
|
if (name.includes('半导体') || name.includes('芯片') || name.includes('封装') ||
|
||||||
'机器人核心零部件': 'pink',
|
name.includes('光刻') || name.includes('硅')) {
|
||||||
'其他类型机器人': 'pink',
|
return 'blue';
|
||||||
// 消费电子相关
|
}
|
||||||
'智能终端': 'cyan',
|
|
||||||
'XR与空间计算': 'cyan',
|
// 机器人相关 - 粉色系
|
||||||
'华为产业链': 'cyan',
|
if (name.includes('机器人') || name.includes('人形')) {
|
||||||
// 智能驾驶相关
|
return 'pink';
|
||||||
'自动驾驶解决方案': 'teal',
|
}
|
||||||
'智能汽车产业链': 'teal',
|
|
||||||
'车路协同': 'teal',
|
// 消费电子 / 手机 / XR - 青色系
|
||||||
// 新能源相关
|
if (name.includes('消费电子') || name.includes('手机') || name.includes('xr') ||
|
||||||
'新型电池技术': 'green',
|
name.includes('华为') || name.includes('苹果') || name.includes('终端')) {
|
||||||
'电力设备与电网': 'green',
|
return 'cyan';
|
||||||
'清洁能源': 'green',
|
}
|
||||||
// 低空与航天
|
|
||||||
'低空经济': 'orange',
|
// 汽车 / 智能驾驶 - 蓝绿色系
|
||||||
'商业航天': 'orange',
|
if (name.includes('汽车') || name.includes('驾驶') || name.includes('新能源车') ||
|
||||||
// 国防军工
|
name.includes('电动车') || name.includes('车路')) {
|
||||||
'无人作战与信息化': 'red',
|
return 'teal';
|
||||||
'海军装备': 'red',
|
}
|
||||||
'军贸出海': 'red',
|
|
||||||
// 医药健康
|
// 新能源 / 电力 / 光伏 - 绿色系
|
||||||
'创新药': 'messenger',
|
if (name.includes('新能源') || name.includes('电力') || name.includes('光伏') ||
|
||||||
'医疗器械': 'messenger',
|
name.includes('储能') || name.includes('电池') || name.includes('风电') ||
|
||||||
'中医药': 'messenger',
|
name.includes('清洁能源')) {
|
||||||
// 消费相关
|
return 'green';
|
||||||
'食品饮料': 'yellow',
|
}
|
||||||
'消费服务': 'yellow',
|
|
||||||
// 传统能源与资源
|
// 低空 / 航天 / 卫星 - 橙色系
|
||||||
'煤炭石油': 'blackAlpha',
|
if (name.includes('低空') || name.includes('航天') || name.includes('卫星') ||
|
||||||
'钢铁建材': 'blackAlpha',
|
name.includes('无人机') || name.includes('飞行')) {
|
||||||
// 公用事业与交运
|
return 'orange';
|
||||||
'公用事业': 'gray',
|
}
|
||||||
'交通运输': 'gray',
|
|
||||||
// 其他
|
// 军工 / 国防 - 红色系
|
||||||
'国家战略': 'red',
|
if (name.includes('军工') || name.includes('国防') || name.includes('军事') ||
|
||||||
'区域发展': 'red',
|
name.includes('武器') || name.includes('海军')) {
|
||||||
'有色金属': 'orange',
|
return 'red';
|
||||||
'化工材料': 'orange',
|
}
|
||||||
'金融科技': 'linkedin',
|
|
||||||
'数字化转型': 'linkedin',
|
// 医药 / 医疗 - messenger蓝
|
||||||
'量子科技': 'purple',
|
if (name.includes('医药') || name.includes('医疗') || name.includes('生物') ||
|
||||||
'脑机接口': 'purple',
|
name.includes('创新药') || name.includes('器械')) {
|
||||||
'国际贸易': 'facebook',
|
return 'messenger';
|
||||||
'宏观主题': 'facebook',
|
}
|
||||||
};
|
|
||||||
return colorMap[lv2Name] || 'gray';
|
// 消费 / 食品 / 零售 - 黄色系
|
||||||
|
if (name.includes('消费') || name.includes('食品') || name.includes('零售') ||
|
||||||
|
name.includes('白酒') || name.includes('饮料')) {
|
||||||
|
return 'yellow';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 传统能源 / 煤炭 / 石油 - 深灰色
|
||||||
|
if (name.includes('煤炭') || name.includes('石油') || name.includes('天然气') ||
|
||||||
|
name.includes('钢铁') || name.includes('有色')) {
|
||||||
|
return 'blackAlpha';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金融 / 银行 / 券商 - linkedin蓝
|
||||||
|
if (name.includes('金融') || name.includes('银行') || name.includes('券商') ||
|
||||||
|
name.includes('保险') || name.includes('证券')) {
|
||||||
|
return 'linkedin';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 政策 / 国家战略 - 红色
|
||||||
|
if (name.includes('政策') || name.includes('战略') || name.includes('国产替代')) {
|
||||||
|
return 'red';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 市场风格 / 题材 - 灰色
|
||||||
|
if (name.includes('市场') || name.includes('风格') || name.includes('题材')) {
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'gray';
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheme = colorScheme || getColorScheme(lv2);
|
const scheme = colorScheme || getColorScheme(lv2);
|
||||||
|
|||||||
Reference in New Issue
Block a user