refactor: 清除前端冗余代码
This commit is contained in:
@@ -9,7 +9,6 @@ interface GraphViewProps {
|
||||
data: GraphData;
|
||||
layout?: GraphLayoutConfig;
|
||||
onNodeClick?: (node: Node | null) => void;
|
||||
onEdgeClick?: (edge: Edge) => void;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
@@ -21,7 +20,6 @@ export const GraphView = ({
|
||||
data,
|
||||
layout = FORCE_LAYOUT_CONFIG,
|
||||
onNodeClick,
|
||||
onEdgeClick,
|
||||
height = GRAPH_DEFAULT_HEIGHT,
|
||||
}: GraphViewProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -31,12 +29,10 @@ export const GraphView = ({
|
||||
const renderPromiseRef = useRef<Promise<void> | null>(null);
|
||||
|
||||
const onNodeClickRef = useRef(onNodeClick);
|
||||
const onEdgeClickRef = useRef(onEdgeClick);
|
||||
|
||||
useEffect(() => {
|
||||
onNodeClickRef.current = onNodeClick;
|
||||
onEdgeClickRef.current = onEdgeClick;
|
||||
}, [onNodeClick, onEdgeClick]);
|
||||
}, [onNodeClick]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
@@ -219,7 +215,7 @@ export const GraphView = ({
|
||||
graphRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [height, layout.type, data.id, onNodeClickRef, onEdgeClickRef]);
|
||||
}, [height, layout.type, data.id, onNodeClickRef]);
|
||||
|
||||
// 窗口大小调整响应
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import type { Node } from '../../types/graph';
|
||||
|
||||
interface NodeTooltipProps {
|
||||
node: Node | null;
|
||||
}
|
||||
|
||||
export const NodeTooltip = ({ node }: NodeTooltipProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsExpanded(false);
|
||||
}, [node?.id]);
|
||||
|
||||
const propertiesList = useMemo(() => {
|
||||
if (!node?.properties) return [];
|
||||
return Object.entries(node.properties);
|
||||
}, [node?.properties]);
|
||||
|
||||
const hasProperties = propertiesList.length > 0;
|
||||
|
||||
if (!node) return null;
|
||||
|
||||
const handleExpand = () => setIsExpanded(true);
|
||||
const handleCollapse = () => setIsExpanded(false);
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-4 right-1/2 translate-x-1/2 bg-white/95 backdrop-blur-sm rounded-lg shadow-lg p-4 max-w-md w-80 z-20">
|
||||
{!isExpanded ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="font-semibold text-base text-gray-800">{node.label}</div>
|
||||
</div>
|
||||
{node.type && (
|
||||
<div className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded inline-block">
|
||||
{node.type}
|
||||
</div>
|
||||
)}
|
||||
{hasProperties && (
|
||||
<div className="space-y-1 text-xs">
|
||||
{propertiesList.slice(0, 2).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between gap-2">
|
||||
<span className="text-gray-500 shrink-0">{key}:</span>
|
||||
<span className="text-gray-800 text-right truncate max-w-[70%]">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{propertiesList.length > 2 && (
|
||||
<div className="text-gray-400 text-center pt-1">还有 {propertiesList.length - 2} 项属性...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasProperties && (
|
||||
<button
|
||||
onClick={handleExpand}
|
||||
className="w-full mt-2 py-1.5 text-xs font-medium text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded transition-colors"
|
||||
>
|
||||
展开查看全部 ▼
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="font-semibold text-base text-gray-800">{node.label}</div>
|
||||
<button
|
||||
onClick={handleCollapse}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-0.5 hover:bg-gray-100 rounded transition-colors"
|
||||
>
|
||||
收起 ▲
|
||||
</button>
|
||||
</div>
|
||||
{node.type && (
|
||||
<div className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded inline-block">
|
||||
{node.type}
|
||||
</div>
|
||||
)}
|
||||
{hasProperties && (
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 uppercase tracking-wide mb-2">
|
||||
属性 ({propertiesList.length})
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs max-h-40 overflow-y-auto">
|
||||
{propertiesList.map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between gap-2 py-0.5">
|
||||
<span className="text-gray-600 shrink-0">{key}:</span>
|
||||
<span className="text-gray-800 text-right break-all max-w-[65%]">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeTooltip;
|
||||
@@ -3,24 +3,7 @@ export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localho
|
||||
export const API_TIMEOUT = 10000;
|
||||
|
||||
// 图谱渲染配置
|
||||
export const GRAPH_CONTAINER_MIN_WIDTH = 600;
|
||||
export const GRAPH_DEFAULT_HEIGHT = 600;
|
||||
|
||||
// 搜索相关常量
|
||||
export const SEARCH_DEBOUNCE_MS = 300;
|
||||
export const SEARCH_RESULT_MAX_HEIGHT = 256; // 16rem
|
||||
|
||||
// UI 相关常量
|
||||
export const NODE_PANEL_WIDTH = 320; // 20rem
|
||||
export const HOVER_PANEL_MAX_WIDTH = 320; // 20rem
|
||||
|
||||
// 性能优化常量
|
||||
export const RESIZE_DEBOUNCE_MS = 200;
|
||||
export const MAX_NODES_FOR_ANIMATION = 500;
|
||||
|
||||
// 错误消息
|
||||
export const ERROR_MESSAGES = {
|
||||
LOAD_GRAPH_FAILED: '加载图谱数据失败',
|
||||
SEARCH_FAILED: '搜索节点失败',
|
||||
NETWORK_ERROR: '网络连接错误',
|
||||
} as const;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { GraphLayoutConfig } from '../types/graph';
|
||||
|
||||
// 支持的布局类型(保留 d3-force 力导向布局)
|
||||
export const SUPPORTED_LAYOUTS = ['d3-force'] as const;
|
||||
|
||||
// 力导向布局默认配置
|
||||
export const FORCE_LAYOUT_CONFIG = {
|
||||
type: 'd3-force' as const,
|
||||
|
||||
@@ -11,7 +11,6 @@ import { resetColorAssignment } from '../config/colors';
|
||||
export const useGraphData = () => {
|
||||
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [hoveredNode, setHoveredNode] = useState<Node | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<SearchNode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -78,11 +77,6 @@ export const useGraphData = () => {
|
||||
console.log('[useGraphData] setSelectedNode called');
|
||||
}, []);
|
||||
|
||||
// 悬停节点(用于图谱悬停)
|
||||
const handleNodeHover = useCallback((node: Node | null) => {
|
||||
setHoveredNode(node);
|
||||
}, []);
|
||||
|
||||
// 监听 selectedNode 变化,用于调试
|
||||
useEffect(() => {
|
||||
console.log('[useGraphData] selectedNode state changed:', selectedNode);
|
||||
@@ -97,13 +91,11 @@ export const useGraphData = () => {
|
||||
return {
|
||||
graphData,
|
||||
selectedNode,
|
||||
hoveredNode,
|
||||
searchResults,
|
||||
loading,
|
||||
setSelectedNode,
|
||||
handleSearch,
|
||||
handleNodeSelectById,
|
||||
handleNodeSelect,
|
||||
handleNodeHover,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { GraphLayoutConfig } from '../types/graph';
|
||||
import { DEFAULT_LAYOUT } from '../config/layout';
|
||||
|
||||
/**
|
||||
* 知识图谱布局管理 Hook
|
||||
* 负责布局状态的管理和切换
|
||||
* 返回固定的默认布局配置
|
||||
*/
|
||||
export const useGraphLayout = () => {
|
||||
const [layout, setLayout] = useState<GraphLayoutConfig>(DEFAULT_LAYOUT);
|
||||
|
||||
// 切换布局类型(目前只支持 force 力导向布局)
|
||||
const handleLayoutChange = useCallback((newLayoutType: GraphLayoutConfig['type']) => {
|
||||
setLayout({ type: newLayoutType });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
layout,
|
||||
handleLayoutChange,
|
||||
layout: {
|
||||
type: 'd3-force' as const,
|
||||
collide: { radius: 60 } as const,
|
||||
link: { distance: 150 } as const,
|
||||
} as GraphLayoutConfig,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import type { GraphData, Node, Edge } from '../types/graph';
|
||||
import type { GraphData, Node } from '../types/graph';
|
||||
import { API_BASE_URL, API_TIMEOUT } from '../config/constants';
|
||||
|
||||
/**
|
||||
@@ -33,21 +33,6 @@ export const graphApi = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索节点(后端搜索)
|
||||
* @param query 搜索关键词
|
||||
* @returns 匹配的节点列表
|
||||
*/
|
||||
async searchNodes(query: string): Promise<Node[]> {
|
||||
try {
|
||||
const response = await apiClient.get<Node[]>(`/search?q=${encodeURIComponent(query)}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to search nodes:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据 ID 获取节点详情
|
||||
* @param id 节点 ID
|
||||
@@ -62,21 +47,6 @@ export const graphApi = {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取节点的邻居节点和连接边
|
||||
* @param nodeId 节点 ID
|
||||
* @returns 包含邻居节点和边的对象
|
||||
*/
|
||||
async getNeighbors(nodeId: string): Promise<{ nodes: Node[]; edges: Edge[] }> {
|
||||
try {
|
||||
const response = await apiClient.get(`/nodes/${nodeId}/neighbors`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch neighbors:', error);
|
||||
return { nodes: [], edges: [] };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default apiClient;
|
||||
@@ -51,13 +51,10 @@ export interface GraphData {
|
||||
|
||||
/**
|
||||
* 图谱布局配置
|
||||
* 支持 force 力导向布局和 d3-force 力导向布局
|
||||
* 支持 d3-force 力导向布局
|
||||
*/
|
||||
export interface GraphLayoutConfig {
|
||||
type: 'force' | 'd3-force';
|
||||
nodeSize?: number;
|
||||
linkDistance?: number;
|
||||
center?: [number, number];
|
||||
type: 'd3-force';
|
||||
collide?: { radius: number; strength?: number };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* 通用工具函数
|
||||
* 虽然当前未被使用,但保留以备未来需求
|
||||
*/
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* 延迟执行函数,如果在延迟时间内再次调用则重置计时器
|
||||
* @param func 要执行的函数
|
||||
* @param wait 延迟时间(毫秒)
|
||||
* @returns 防抖后的函数
|
||||
*/
|
||||
export const debounce = <T extends (...args: unknown[]) => unknown>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* 限制函数执行频率,在指定时间内只执行一次
|
||||
* @param func 要执行的函数
|
||||
* @param limit 时间间隔(毫秒)
|
||||
* @returns 节流后的函数
|
||||
*/
|
||||
export const throttle = <T extends (...args: unknown[]) => unknown>(
|
||||
func: T,
|
||||
limit: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let inThrottle: boolean = false;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成 UUID
|
||||
* @returns UUID 字符串
|
||||
*/
|
||||
export const generateUUID = (): string => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据重要性计算节点大小
|
||||
* @param importance 重要性系数(默认为 1)
|
||||
* @returns 节点大小
|
||||
*/
|
||||
export const calculateNodeSize = (importance: number = 1): number => {
|
||||
const baseSize = 40;
|
||||
const multiplier = importance * 20;
|
||||
return baseSize + multiplier;
|
||||
};
|
||||
Reference in New Issue
Block a user