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 }); + }, +}));