perfect: 设定样式资源池将硬编码label转化为可配置
This commit is contained in:
@@ -1,15 +1,27 @@
|
|||||||
import { useState } from 'react';
|
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 [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
|
||||||
const nodeTypes = Object.entries(NODE_COLOR_MAP);
|
if (!legendData || legendData.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm rounded-lg shadow-lg p-4 transition-all duration-300 hover:shadow-xl z-10">
|
<div className="absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm rounded-lg shadow-lg p-4 transition-all duration-300 hover:shadow-xl z-10">
|
||||||
@@ -33,33 +45,30 @@ export const Legend = () => {
|
|||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{nodeTypes.map(([type, colors], index) => {
|
{legendData.map((item, index) => (
|
||||||
const labels = NODE_TYPE_LABELS[type] || { zh: type, en: type };
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
key={type}
|
key={item.type}
|
||||||
className="flex items-center space-x-3 p-2 rounded hover:bg-gray-50 transition-colors cursor-pointer"
|
className="flex items-center space-x-3 p-2 rounded hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
style={{ animationDelay: `${index * 50}ms` }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded-full flex-shrink-0 shadow-sm"
|
className="w-4 h-4 rounded-full flex-shrink-0 shadow-sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.fill,
|
backgroundColor: item.colors.fill,
|
||||||
border: `1px solid ${colors.stroke}`,
|
border: `1px solid ${item.colors.stroke}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="text-xs font-medium text-gray-800 truncate">
|
<span className="text-xs font-medium text-gray-800 truncate">
|
||||||
{labels.zh}
|
{item.labels.zh}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-gray-500 truncate">
|
<span className="text-[10px] text-gray-500 truncate">
|
||||||
{labels.en}
|
{item.labels.en}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+116
-13
@@ -1,30 +1,133 @@
|
|||||||
import type { Node } from '../types/graph';
|
import type { Node, GraphData } from '../types/graph';
|
||||||
|
|
||||||
// 节点类型颜色配置
|
interface ColorConfig {
|
||||||
// 用于统一管理知识图谱中不同类型节点的视觉样式
|
fill: string;
|
||||||
export const NODE_COLOR_MAP: Record<string, { fill: string; stroke: string }> = {
|
stroke: string;
|
||||||
'概念': { fill: '#4ECDC4', stroke: '#45B7B0' },
|
}
|
||||||
'工具': { fill: '#FFA07A', stroke: '#FF7F50' },
|
|
||||||
'应用': { fill: '#2ECC71', stroke: '#27AE60' },
|
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<string, { zh: string; en: string }> = {
|
export const NODE_TYPE_LABELS: Record<string, { zh: string; en: string }> = {
|
||||||
'概念': { zh: '概念', en: 'Concept' },
|
'概念': { zh: '概念', en: 'Concept' },
|
||||||
'工具': { zh: '工具', en: 'Tool' },
|
'工具': { zh: '工具', en: 'Tool' },
|
||||||
'应用': { zh: '应用', en: 'Application' },
|
'应用': { 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<string, ColorConfig> = 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<string>();
|
||||||
|
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;
|
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 } => {
|
export const getNodeLabels = (type: string): { zh: string; en: string } => {
|
||||||
return NODE_TYPE_LABELS[type] || { zh: type, en: type };
|
return NODE_TYPE_LABELS[type] || { zh: type, en: type };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
|
|||||||
import type { GraphData, Node, SearchNode } from '../types/graph';
|
import type { GraphData, Node, SearchNode } from '../types/graph';
|
||||||
import { graphApi } from '../services/graphApi';
|
import { graphApi } from '../services/graphApi';
|
||||||
import { SAMPLE_GRAPH_DATA } from '../config/sampleData';
|
import { SAMPLE_GRAPH_DATA } from '../config/sampleData';
|
||||||
|
import { resetColorAssignment } from '../config/colors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识图谱数据管理 Hook
|
* 知识图谱数据管理 Hook
|
||||||
@@ -20,6 +21,9 @@ export const useGraphData = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await graphApi.getData();
|
const data = await graphApi.getData();
|
||||||
|
|
||||||
|
// 重置颜色分配缓存,确保每次加载数据时颜色分配一致
|
||||||
|
resetColorAssignment();
|
||||||
|
|
||||||
// 如果后端返回空数据,使用示例数据
|
// 如果后端返回空数据,使用示例数据
|
||||||
if (data.nodes.length === 0) {
|
if (data.nodes.length === 0) {
|
||||||
setGraphData(SAMPLE_GRAPH_DATA);
|
setGraphData(SAMPLE_GRAPH_DATA);
|
||||||
@@ -28,6 +32,8 @@ export const useGraphData = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load graph data:', error);
|
console.error('Failed to load graph data:', error);
|
||||||
|
// 重置颜色分配缓存
|
||||||
|
resetColorAssignment();
|
||||||
// API 失败时使用示例数据作为降级方案
|
// API 失败时使用示例数据作为降级方案
|
||||||
setGraphData(SAMPLE_GRAPH_DATA);
|
setGraphData(SAMPLE_GRAPH_DATA);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import GraphView from '../components/GraphView/index';
|
import GraphView from '../components/GraphView/index';
|
||||||
import SearchBar from '../components/SearchBar/index';
|
import SearchBar from '../components/SearchBar/index';
|
||||||
import NodePanel from '../components/NodePanel/index';
|
import NodePanel from '../components/NodePanel/index';
|
||||||
import Legend from '../components/Legend/index';
|
import Legend from '../components/Legend/index';
|
||||||
import { useGraphData } from '../hooks/useGraphData';
|
import { useGraphData } from '../hooks/useGraphData';
|
||||||
import { useGraphLayout } from '../hooks/useGraphLayout';
|
import { useGraphLayout } from '../hooks/useGraphLayout';
|
||||||
|
import { generateLegendData } from '../config/colors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识图谱主页面组件
|
* 知识图谱主页面组件
|
||||||
@@ -27,6 +29,13 @@ export const KnowledgeGraph = () => {
|
|||||||
|
|
||||||
const { layout } = useGraphLayout();
|
const { layout } = useGraphLayout();
|
||||||
|
|
||||||
|
const legendData = useMemo(() => {
|
||||||
|
if (!graphData || graphData.nodes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return generateLegendData(graphData);
|
||||||
|
}, [graphData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen flex flex-col bg-gray-50">
|
<div className="w-full h-screen flex flex-col bg-gray-50">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -54,7 +63,7 @@ export const KnowledgeGraph = () => {
|
|||||||
|
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
<main className="flex-1 bg-white relative">
|
<main className="flex-1 bg-white relative">
|
||||||
<Legend />
|
{legendData.length > 0 && <Legend legendData={legendData} />}
|
||||||
<GraphView
|
<GraphView
|
||||||
data={graphData}
|
data={graphData}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
|
|||||||
Reference in New Issue
Block a user