From 82959dad19b2a96b8130a7fa1c14e1a8e0e04df0 Mon Sep 17 00:00:00 2001
From: hhs <386998068.com>
Date: Fri, 10 Apr 2026 14:40:13 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AF=B9=E5=89=8D?=
=?UTF-8?q?=E7=AB=AF=E7=94=A8Zustand=E8=BF=9B=E8=A1=8C=E7=8A=B6=E6=80=81?=
=?UTF-8?q?=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/package-lock.json | 32 ++++++-
frontend/package.json | 3 +-
frontend/src/components/NodePanel/index.tsx | 35 ++++---
frontend/src/components/SearchBar/index.tsx | 29 ++----
frontend/src/hooks/useGraphData.ts | 101 --------------------
frontend/src/hooks/useGraphLayout.ts | 15 ---
frontend/src/pages/KnowledgeGraph.tsx | 32 +++----
frontend/src/store/graphStore.ts | 91 ++++++++++++++++++
frontend/src/store/index.ts | 2 +
frontend/src/store/layoutStore.ts | 19 ++++
10 files changed, 182 insertions(+), 177 deletions(-)
delete mode 100644 frontend/src/hooks/useGraphData.ts
delete mode 100644 frontend/src/hooks/useGraphLayout.ts
create mode 100644 frontend/src/store/graphStore.ts
create mode 100644 frontend/src/store/index.ts
create mode 100644 frontend/src/store/layoutStore.ts
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9a2058f..1c9e256 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -18,7 +18,8 @@
"postcss": "^8.5.8",
"react": "^19.2.4",
"react-dom": "^19.2.4",
- "tailwindcss": "^4.2.2"
+ "tailwindcss": "^4.2.2",
+ "zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
@@ -6816,6 +6817,35 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.12.tgz",
+ "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 1596441..d9deeef 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -20,7 +20,8 @@
"postcss": "^8.5.8",
"react": "^19.2.4",
"react-dom": "^19.2.4",
- "tailwindcss": "^4.2.2"
+ "tailwindcss": "^4.2.2",
+ "zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
diff --git a/frontend/src/components/NodePanel/index.tsx b/frontend/src/components/NodePanel/index.tsx
index 1e495ee..6d528ed 100644
--- a/frontend/src/components/NodePanel/index.tsx
+++ b/frontend/src/components/NodePanel/index.tsx
@@ -1,16 +1,15 @@
-import type { Node } from '../../types/graph';
-
-interface NodePanelProps {
- node: Node | null;
- onClose?: () => void;
-}
+import { useGraphStore } from '../../store';
/**
* 节点详情面板组件
* 展示选中节点的详细信息,包括属性和样式
*/
-export const NodePanel = ({ node, onClose }: NodePanelProps) => {
- if (!node) return null;
+export const NodePanel = () => {
+ const { selectedNode, setSelectedNode } = useGraphStore();
+
+ if (!selectedNode) return null;
+
+ const node = selectedNode;
// 判断属性是否可展示
const hasProperties = node.properties && Object.keys(node.properties).length > 0;
@@ -19,17 +18,15 @@ export const NodePanel = ({ node, onClose }: NodePanelProps) => {
节点详情
- {onClose && (
-
- )}
+
diff --git a/frontend/src/components/SearchBar/index.tsx b/frontend/src/components/SearchBar/index.tsx
index 31325e7..f55b728 100644
--- a/frontend/src/components/SearchBar/index.tsx
+++ b/frontend/src/components/SearchBar/index.tsx
@@ -1,24 +1,13 @@
import { useState, type ChangeEvent, type KeyboardEvent } from 'react';
-import type { SearchNode } from '../../types/graph';
+import { useGraphStore } from '../../store';
import { SEARCH_RESULT_MAX_HEIGHT } from '../../config/constants';
-interface SearchBarProps {
- onSearch?: (query: string) => void;
- onNodeSelect?: (nodeId: string) => void;
- searchResults?: SearchNode[];
- placeholder?: string;
-}
-
/**
* 搜索栏组件
* 支持实时搜索和结果展示
*/
-export const SearchBar = ({
- onSearch,
- onNodeSelect,
- searchResults = [],
- placeholder = '搜索节点...',
-}: SearchBarProps) => {
+export const SearchBar = ({ placeholder = '搜索节点...' }: { placeholder?: string }) => {
+ const { handleSearch, handleNodeSelectById, searchResults } = useGraphStore();
const [query, setQuery] = useState('');
const [showResults, setShowResults] = useState(false);
@@ -26,8 +15,8 @@ export const SearchBar = ({
const handleInputChange = (e: ChangeEvent) => {
const value = e.target.value;
setQuery(value);
- if (onSearch && value.trim()) {
- onSearch(value.trim());
+ if (value.trim()) {
+ handleSearch(value.trim());
setShowResults(true);
} else {
setShowResults(false);
@@ -37,8 +26,8 @@ export const SearchBar = ({
// 处理键盘事件(Enter 确认搜索)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
- if (onSearch && query.trim()) {
- onSearch(query.trim());
+ if (query.trim()) {
+ handleSearch(query.trim());
}
setShowResults(false);
}
@@ -46,9 +35,7 @@ export const SearchBar = ({
// 选择搜索结果中的节点
const handleNodeClick = (nodeId: string) => {
- if (onNodeSelect) {
- onNodeSelect(nodeId);
- }
+ handleNodeSelectById(nodeId);
setShowResults(false);
setQuery('');
};
diff --git a/frontend/src/hooks/useGraphData.ts b/frontend/src/hooks/useGraphData.ts
deleted file mode 100644
index a569397..0000000
--- a/frontend/src/hooks/useGraphData.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-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
- * 负责图谱数据的加载、搜索和节点选择
- */
-export const useGraphData = () => {
- const [graphData, setGraphData] = useState({ nodes: [], edges: [] });
- const [selectedNode, setSelectedNode] = useState(null);
- const [searchResults, setSearchResults] = useState([]);
- const [loading, setLoading] = useState(true);
-
- // 从后端加载图谱数据
- const loadGraphData = useCallback(async () => {
- try {
- setLoading(true);
- const data = await graphApi.getData();
-
- // 重置颜色分配缓存,确保每次加载数据时颜色分配一致
- resetColorAssignment();
-
- // 如果后端返回空数据,使用示例数据
- if (data.nodes.length === 0) {
- setGraphData({ ...SAMPLE_GRAPH_DATA, id: `sample-${Date.now()}` });
- } else {
- setGraphData({ ...data, id: `data-${Date.now()}` });
- }
- } catch (error) {
- console.error('Failed to load graph data:', error);
- // 重置颜色分配缓存
- resetColorAssignment();
- // API 失败时使用示例数据作为降级方案
- setGraphData({ ...SAMPLE_GRAPH_DATA, id: `fallback-${Date.now()}` });
- } finally {
- setLoading(false);
- }
- }, []);
-
- // 搜索节点(前端过滤,可根据需要切换到后端搜索)
- const handleSearch = useCallback((query: string) => {
- const results = graphData.nodes
- .filter((node: Node) =>
- node.label.toLowerCase().includes(query.toLowerCase()) ||
- node.id.toLowerCase().includes(query.toLowerCase())
- )
- .map((node: Node) => ({
- id: node.id,
- label: node.label,
- type: node.type,
- matched: true,
- }));
- setSearchResults(results);
- }, [graphData]);
-
- // 通过 ID 选择节点(用于搜索结果点击)
- const handleNodeSelectById = useCallback((nodeId: string) => {
- console.log('[useGraphData] handleNodeSelectById called with id:', nodeId);
- const node = graphData.nodes.find((n: Node) => n.id === nodeId);
- console.log('[useGraphData] Found node:', node);
- if (node) {
- console.log('[useGraphData] Setting selectedNode:', node);
- setSelectedNode(node);
- } else {
- console.log('[useGraphData] Node not found with id:', nodeId);
- }
- }, [graphData.nodes]);
-
- // 选择节点(用于图谱点击)
- const handleNodeSelect = useCallback((node: Node | null) => {
- console.log('[useGraphData] handleNodeSelect called with node:', node);
- console.log('[useGraphData] About to call setSelectedNode');
- setSelectedNode(node);
- console.log('[useGraphData] setSelectedNode called');
- }, []);
-
- // 监听 selectedNode 变化,用于调试
- useEffect(() => {
- console.log('[useGraphData] selectedNode state changed:', selectedNode);
- console.log('[useGraphData] selectedNode truthiness:', !!selectedNode);
- }, [selectedNode]);
-
- // 组件挂载时加载数据
- useEffect(() => {
- loadGraphData();
- }, [loadGraphData]);
-
- return {
- graphData,
- selectedNode,
- searchResults,
- loading,
- setSelectedNode,
- handleSearch,
- handleNodeSelectById,
- handleNodeSelect,
- };
-};
diff --git a/frontend/src/hooks/useGraphLayout.ts b/frontend/src/hooks/useGraphLayout.ts
deleted file mode 100644
index 7a66069..0000000
--- a/frontend/src/hooks/useGraphLayout.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { GraphLayoutConfig } from '../types/graph';
-
-/**
- * 知识图谱布局管理 Hook
- * 返回固定的默认布局配置
- */
-export const useGraphLayout = () => {
- return {
- layout: {
- type: 'd3-force' as const,
- collide: { radius: 60 } as const,
- link: { distance: 150 } as const,
- } as GraphLayoutConfig,
- };
-};
diff --git a/frontend/src/pages/KnowledgeGraph.tsx b/frontend/src/pages/KnowledgeGraph.tsx
index 4625f7b..20a68b4 100644
--- a/frontend/src/pages/KnowledgeGraph.tsx
+++ b/frontend/src/pages/KnowledgeGraph.tsx
@@ -1,10 +1,10 @@
-import { useMemo } from 'react';
+import { useMemo, useEffect } 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 { useGraphStore } from '../store';
+import { useLayoutStore } from '../store';
import { generateLegendData } from '../config/colors';
/**
@@ -18,15 +18,16 @@ export const KnowledgeGraph = () => {
const {
graphData,
selectedNode,
- searchResults,
- loading,
- setSelectedNode,
- handleSearch,
- handleNodeSelectById,
+ loadGraphData,
handleNodeSelect,
- } = useGraphData();
+ loading,
+ } = useGraphStore();
- const { layout } = useGraphLayout();
+ const { layout } = useLayoutStore();
+
+ useEffect(() => {
+ loadGraphData();
+ }, [loadGraphData]);
const legendData = useMemo(() => {
if (!graphData || graphData.nodes.length === 0) {
@@ -52,11 +53,7 @@ export const KnowledgeGraph = () => {
-
+
@@ -73,10 +70,7 @@ export const KnowledgeGraph = () => {
{selectedNode && (
- setSelectedNode(null)}
- />
+
)}
>
diff --git a/frontend/src/store/graphStore.ts b/frontend/src/store/graphStore.ts
new file mode 100644
index 0000000..58ffdba
--- /dev/null
+++ b/frontend/src/store/graphStore.ts
@@ -0,0 +1,91 @@
+import { create } from 'zustand';
+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';
+
+interface GraphState {
+ graphData: GraphData;
+ selectedNode: Node | null;
+ searchResults: SearchNode[];
+ loading: boolean;
+ addGraphData: (data: GraphData) => void;
+ setSelectedNode: (node: Node | null) => void;
+ loadGraphData: () => Promise
;
+ handleSearch: (query: string) => void;
+ handleNodeSelect: (node: Node | null) => void;
+ handleNodeSelectById: (nodeId: string) => void;
+}
+
+export const useGraphStore = create((set, get) => ({
+ graphData: { nodes: [], edges: [] },
+ selectedNode: null,
+ searchResults: [],
+ loading: true,
+
+ addGraphData: (data: GraphData) => {
+ set({ graphData: data });
+ },
+
+ setSelectedNode: (node: Node | null) => {
+ console.log('[graphStore] setSelectedNode called with:', node);
+ set({ selectedNode: node });
+ },
+
+ loadGraphData: async () => {
+ try {
+ set({ loading: true });
+ const data = await graphApi.getData();
+
+ resetColorAssignment();
+
+ if (data.nodes.length === 0) {
+ set({ graphData: { ...SAMPLE_GRAPH_DATA, id: `sample-${Date.now()}` } });
+ } else {
+ set({ graphData: { ...data, id: `data-${Date.now()}` } });
+ }
+ } catch (error) {
+ console.error('Failed to load graph data:', error);
+ resetColorAssignment();
+ set({ graphData: { ...SAMPLE_GRAPH_DATA, id: `fallback-${Date.now()}` } });
+ } finally {
+ set({ loading: false });
+ }
+ },
+
+ handleSearch: (query: string) => {
+ const { graphData } = get();
+ const results = graphData.nodes
+ .filter((node: Node) =>
+ node.label.toLowerCase().includes(query.toLowerCase()) ||
+ node.id.toLowerCase().includes(query.toLowerCase())
+ )
+ .map((node: Node) => ({
+ id: node.id,
+ label: node.label,
+ type: node.type,
+ matched: true,
+ }));
+ set({ searchResults: results });
+ },
+
+ handleNodeSelectById: (nodeId: string) => {
+ console.log('[graphStore] handleNodeSelectById called with id:', nodeId);
+ const { graphData } = get();
+ const node = graphData.nodes.find((n: Node) => n.id === nodeId);
+ console.log('[graphStore] Found node:', node);
+ if (node) {
+ console.log('[graphStore] Setting selectedNode:', node);
+ set({ selectedNode: node });
+ } else {
+ console.log('[graphStore] Node not found with id:', nodeId);
+ }
+ },
+
+ handleNodeSelect: (node: Node | null) => {
+ console.log('[graphStore] handleNodeSelect called with node:', node);
+ console.log('[graphStore] About to set selectedNode');
+ set({ selectedNode: node });
+ console.log('[graphStore] selectedNode set');
+ },
+}));
diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts
new file mode 100644
index 0000000..7d61535
--- /dev/null
+++ b/frontend/src/store/index.ts
@@ -0,0 +1,2 @@
+export { useGraphStore } from './graphStore';
+export { useLayoutStore } from './layoutStore';
diff --git a/frontend/src/store/layoutStore.ts b/frontend/src/store/layoutStore.ts
new file mode 100644
index 0000000..c61c8d8
--- /dev/null
+++ b/frontend/src/store/layoutStore.ts
@@ -0,0 +1,19 @@
+import { create } from 'zustand';
+import type { GraphLayoutConfig } from '../types/graph';
+
+interface LayoutState {
+ layout: GraphLayoutConfig;
+ setLayout: (layout: GraphLayoutConfig) => void;
+}
+
+export const useLayoutStore = create((set) => ({
+ layout: {
+ type: 'd3-force' as const,
+ collide: { radius: 60 } as const,
+ link: { distance: 150 } as const,
+ } as GraphLayoutConfig,
+
+ setLayout: (layout: GraphLayoutConfig) => {
+ set({ layout });
+ },
+}));