From dca7f5f35c038bac18519a2f7e860a5803f3569a Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 6 Apr 2026 14:37:02 +0800 Subject: [PATCH] =?UTF-8?q?perfect:=20=E8=AE=BE=E5=AE=9A=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=B1=A0=E5=B0=86=E7=A1=AC=E7=BC=96=E7=A0=81?= =?UTF-8?q?label=E8=BD=AC=E5=8C=96=E4=B8=BA=E5=8F=AF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Legend/index.tsx | 63 ++++++----- frontend/src/config/colors.ts | 129 ++++++++++++++++++++--- frontend/src/hooks/useGraphData.ts | 6 ++ frontend/src/pages/KnowledgeGraph.tsx | 11 +- 4 files changed, 168 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/Legend/index.tsx b/frontend/src/components/Legend/index.tsx index 5d1891d..e8a68a8 100644 --- a/frontend/src/components/Legend/index.tsx +++ b/frontend/src/components/Legend/index.tsx @@ -1,15 +1,27 @@ import { useState } from 'react'; -import { NODE_COLOR_MAP, NODE_TYPE_LABELS } from '../../config/colors'; + +interface LegendData { + type: string; + colors: { fill: string; stroke: string }; + labels: { zh: string; en: string }; +} + +interface LegendProps { + legendData: LegendData[]; +} /** * 图例组件 * 显示知识图谱中不同节点类型及其对应的颜色 * 支持折叠/展开展示 + * @param {LegendData[]} legendData - 图例数据,包含类型、颜色和标签信息 */ -export const Legend = () => { +export const Legend = ({ legendData }: LegendProps) => { const [isExpanded, setIsExpanded] = useState(true); - const nodeTypes = Object.entries(NODE_COLOR_MAP); + if (!legendData || legendData.length === 0) { + return null; + } return (
@@ -33,33 +45,30 @@ export const Legend = () => { {isExpanded && (
- {nodeTypes.map(([type, colors], index) => { - const labels = NODE_TYPE_LABELS[type] || { zh: type, en: type }; - return ( + {legendData.map((item, index) => ( +
-
+ className="w-4 h-4 rounded-full flex-shrink-0 shadow-sm" + style={{ + backgroundColor: item.colors.fill, + border: `1px solid ${item.colors.stroke}`, + }} + /> -
- - {labels.zh} - - - {labels.en} - -
+
+ + {item.labels.zh} + + + {item.labels.en} +
- ); - })} +
+ ))}
)}
diff --git a/frontend/src/config/colors.ts b/frontend/src/config/colors.ts index ab0975d..17863ec 100644 --- a/frontend/src/config/colors.ts +++ b/frontend/src/config/colors.ts @@ -1,30 +1,133 @@ -import type { Node } from '../types/graph'; +import type { Node, GraphData } from '../types/graph'; -// 节点类型颜色配置 -// 用于统一管理知识图谱中不同类型节点的视觉样式 -export const NODE_COLOR_MAP: Record = { - '概念': { fill: '#4ECDC4', stroke: '#45B7B0' }, - '工具': { fill: '#FFA07A', stroke: '#FF7F50' }, - '应用': { fill: '#2ECC71', stroke: '#27AE60' }, -}; +interface ColorConfig { + fill: string; + stroke: string; +} + +interface NodeTypeInfo { + type: string; + colors: ColorConfig; + labels: { zh: string; en: string }; +} + +// 预定义颜色池 - 12种可区分的颜色对 +const COLOR_POOL: ColorConfig[] = [ + { fill: '#4ECDC4', stroke: '#45B7B0' }, + { fill: '#FFA07A', stroke: '#FF7F50' }, + { fill: '#2ECC71', stroke: '#27AE60' }, + { fill: '#FF6B9D', stroke: '#E84393' }, + { fill: '#F39C12', stroke: '#D68910' }, + { fill: '#9B59B6', stroke: '#8E44AD' }, + { fill: '#3498DB', stroke: '#2980B9' }, + { fill: '#1ABC9C', stroke: '#16A085' }, + { fill: '#E74C3C', stroke: '#C0392B' }, + { fill: '#607D8B', stroke: '#455A64' }, + { fill: '#FF9800', stroke: '#F57C00' }, + { fill: '#3F51B5', stroke: '#303F9F' }, +]; // 默认节点颜色(当类型未匹配时) -export const DEFAULT_NODE_COLORS = { fill: '#C6E5FF', stroke: '#5B8FF9' }; +export const DEFAULT_NODE_COLORS: ColorConfig = { fill: '#C6E5FF', stroke: '#5B8FF9' }; // 节点类型双语标签映射 export const NODE_TYPE_LABELS: Record = { '概念': { zh: '概念', en: 'Concept' }, '工具': { zh: '工具', en: 'Tool' }, '应用': { zh: '应用', en: 'Application' }, + '属性': { zh: '属性', en: 'Attribute' }, + '实体': { zh: '实体', en: 'Entity' }, + '关系': { zh: '关系', en: 'Relation' }, + '方法': { zh: '方法', en: 'Method' }, + '函数': { zh: '函数', en: 'Function' }, + '类': { zh: '类', en: 'Class' }, + '模块': { zh: '模块', en: 'Module' }, }; -// 获取节点颜色配置 -export const getNodeColors = (node: Node): { fill: string; stroke: string } => { +// 动态颜色分配缓存 +const colorAssignmentCache: Map = new Map(); +let colorPoolIndex = 0; + +/** + * 从颜色池中获取或分配颜色 + * @param type 节点类型 + * @returns 颜色配置对象 + */ +const getOrAssignColor = (type: string): ColorConfig => { + if (!type) return DEFAULT_NODE_COLORS; + + // 从缓存中查找 + if (colorAssignmentCache.has(type)) { + return colorAssignmentCache.get(type)!; + } + + // 从颜色池中分配新颜色 + const colorConfig = COLOR_POOL[colorPoolIndex % COLOR_POOL.length]; + colorAssignmentCache.set(type, colorConfig); + colorPoolIndex++; + + return colorConfig; +}; + +/** + * 重置颜色分配缓存 + * 用于重新加载图谱数据时清除之前的分配 + */ +export const resetColorAssignment = (): void => { + colorAssignmentCache.clear(); + colorPoolIndex = 0; +}; + +/** + * 从图谱数据中提取所有唯一的节点类型 + * @param graphData 图谱数据 + * @returns 唯一的节点类型数组 + */ +export const extractNodeTypes = (graphData: GraphData): string[] => { + const typeSet = new Set(); + graphData.nodes.forEach((node) => { + if (node.type) { + typeSet.add(node.type); + } + }); + return Array.from(typeSet); +}; + +/** + * 生成图例数据 + * @param graphData 图谱数据 + * @returns 图例数据数组 + */ +export const generateLegendData = (graphData: GraphData): NodeTypeInfo[] => { + const nodeTypes = extractNodeTypes(graphData); + + return nodeTypes.map((type) => { + const colors = getOrAssignColor(type); + const labels = NODE_TYPE_LABELS[type] || { zh: type, en: type }; + + return { + type, + colors, + labels, + }; + }); +}; + +/** + * 获取节点颜色配置 + * @param node 节点对象 + * @returns 颜色配置对象 + */ +export const getNodeColors = (node: Node): ColorConfig => { if (!node.type) return DEFAULT_NODE_COLORS; - return NODE_COLOR_MAP[node.type] || DEFAULT_NODE_COLORS; + return getOrAssignColor(node.type); }; -// 获取节点双语标签 +/** + * 获取节点双语标签 + * @param type 节点类型 + * @returns 双语标签对象 + */ export const getNodeLabels = (type: string): { zh: string; en: string } => { return NODE_TYPE_LABELS[type] || { zh: type, en: type }; }; diff --git a/frontend/src/hooks/useGraphData.ts b/frontend/src/hooks/useGraphData.ts index ff97b9d..b626d92 100644 --- a/frontend/src/hooks/useGraphData.ts +++ b/frontend/src/hooks/useGraphData.ts @@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react'; import type { GraphData, Node, SearchNode } from '../types/graph'; import { graphApi } from '../services/graphApi'; import { SAMPLE_GRAPH_DATA } from '../config/sampleData'; +import { resetColorAssignment } from '../config/colors'; /** * 知识图谱数据管理 Hook @@ -20,6 +21,9 @@ export const useGraphData = () => { setLoading(true); const data = await graphApi.getData(); + // 重置颜色分配缓存,确保每次加载数据时颜色分配一致 + resetColorAssignment(); + // 如果后端返回空数据,使用示例数据 if (data.nodes.length === 0) { setGraphData(SAMPLE_GRAPH_DATA); @@ -28,6 +32,8 @@ export const useGraphData = () => { } } catch (error) { console.error('Failed to load graph data:', error); + // 重置颜色分配缓存 + resetColorAssignment(); // API 失败时使用示例数据作为降级方案 setGraphData(SAMPLE_GRAPH_DATA); } finally { diff --git a/frontend/src/pages/KnowledgeGraph.tsx b/frontend/src/pages/KnowledgeGraph.tsx index ff374dc..e4adebd 100644 --- a/frontend/src/pages/KnowledgeGraph.tsx +++ b/frontend/src/pages/KnowledgeGraph.tsx @@ -1,9 +1,11 @@ +import { useMemo } from 'react'; import GraphView from '../components/GraphView/index'; import SearchBar from '../components/SearchBar/index'; import NodePanel from '../components/NodePanel/index'; import Legend from '../components/Legend/index'; import { useGraphData } from '../hooks/useGraphData'; import { useGraphLayout } from '../hooks/useGraphLayout'; +import { generateLegendData } from '../config/colors'; /** * 知识图谱主页面组件 @@ -27,6 +29,13 @@ export const KnowledgeGraph = () => { const { layout } = useGraphLayout(); + const legendData = useMemo(() => { + if (!graphData || graphData.nodes.length === 0) { + return []; + } + return generateLegendData(graphData); + }, [graphData]); + return (
{loading ? ( @@ -54,7 +63,7 @@ export const KnowledgeGraph = () => {
- + {legendData.length > 0 && }